PluginProbe
PayPlug for WooCommerce (Official) / 3.1.0
PayPlug for WooCommerce (Official) v3.1.0
3.1.0 3.0.0 2.18.0 1.0.17 1.0.18 1.0.19 1.0.20 1.0.21 1.0.22 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1.0 1.10.0 1.10.1 1.2.1 1.2.10 1.2.11 1.2.2 1.2.3 1.2.4 1.2.5 All 102 releases
← All changes | src/Controller/ApplePay.php +704 -241 1.10.0 → 3.1.0 View file →
@@ -1,301 +1,764 @@
1 1 <?php
2 2
3 3 namespace Payplug\PayplugWoocommerce\Controller;
4 4
5 +use function is_cart;
6 +use function is_product;
7 +
5 8 use Payplug\Exception\HttpException;
6 9 use Payplug\PayplugWoocommerce\Gateway\PayplugAddressData;
10 +use Payplug\PayplugWoocommerce\Gateway\PayplugGateway;
7 11 use Payplug\PayplugWoocommerce\PayplugWoocommerceHelper;
8 -use Payplug\PayplugWoocommerce\Gateway\PayplugGateway;
9 12 use Payplug\Resource\Payment as PaymentResource;
10 13
14 +/**
15 + * ApplePay controller for handling Apple Pay payment logic in WooCommerce.
16 + */
11 17 class ApplePay extends PayplugGateway
12 18 {
19 + public $domain_name = '';
13 20
14 - protected $enable = false;
15 - protected $domain_name = "";
21 + protected $cart = false;
16 22
17 - public function __construct()
18 - {
23 + protected $checkout = false;
19 24
20 - parent::__construct();
25 + protected $carriers = [];
21 26
22 - /** @var \WC_Settings_API override $id */
23 - $this->id = 'apple_pay';
27 + public const ENABLE_ON_TEST_MODE = false;
24 28
25 - /** @var \WC_Payment_Gateway overwrite for apple pay settings */
26 - $this->method_title = __('payplug_apple_pay_title', 'payplug');
27 - $this->method_description = "";
29 + public $image = 'apple-pay-checkout.svg';
28 30
29 - $this->title = __('payplug_apple_pay_title', 'payplug');
30 - $this->description = '<div id="apple-pay-button-wrapper"><apple-pay-button buttonstyle="black" type="pay" locale="'. get_locale() .'"></apple-pay-button></div>';
31 - $this->domain_name = strtr(get_site_url(), array("http://" => "", "https://" => ""));
31 + protected $product = false;
32 32
33 - if (!($this->checkApplePay() && $this->checkDeviceComptability() && $this->isSSL())) {
34 - $this->enabled = 'no';
33 + public function __construct()
34 + {
35 + parent::__construct();
35 36
36 - } else {
37 - add_action('wp_enqueue_scripts', [$this, 'add_apple_pay_css']);
38 - add_action('wp_enqueue_scripts', [$this, 'add_apple_pay_js']);
39 - }
37 + /** @var \WC_Settings_API override $id */
38 + $this->id = 'apple_pay';
40 39
41 - }
40 + /** @var \WC_Payment_Gateway overwrite for apple pay settings */
41 + $this->method_title = __('payplug_apple_pay_title', 'payplug');
42 + $this->method_description = '';
43 + $this->has_fields = false;
42 44
43 - /**
44 - * @return bool|void
45 - */
46 - public function process_admin_options() {
47 - $data = $this->get_post_data();
48 - if (isset($data['woocommerce_payplug_mode'])) {
49 - if ( $this->get_post_data()['woocommerce_payplug_mode'] === '0' ) {
50 - $options = get_option( 'woocommerce_payplug_settings', [] );
51 - $options['apple_pay'] = 'no';
52 - update_option( 'woocommerce_payplug_settings', apply_filters( 'woocommerce_settings_api_sanitized_fields_payplug', $options ) );
53 - }
54 - }
55 - if (isset($data['woocommerce_payplug_apple_pay'])) {
56 - if (($data['woocommerce_payplug_apple_pay'] == 1) && (!$this->checkApplePay())) {
57 - add_action( 'admin_notices', [$this ,"display_notice"] );
58 - }
59 - }
45 + $this->title = __('payplug_apple_pay_title', 'payplug');
46 + $this->description = '<div id="apple-pay-button-wrapper"><apple-pay-button buttonstyle="black" type="pay" locale="' . get_locale() . '"></apple-pay-button></div>';
47 + $this->domain_name = wp_parse_url(home_url(), PHP_URL_HOST);
48 + $this->enabled = 'no';
60 49
61 - }
50 + if ($this->checkApplePay() && is_admin()) {
51 + $this->enabled = 'yes';
52 + } elseif ($this->checkApplePay() && $this->isSSL()) {
53 + if (!is_admin() && $this->get_button_checkout()) {
54 + $this->enabled = 'yes';
55 + }
62 56
63 - /**
64 - *
65 - * Check Apple Pay Authorization
66 - *
67 - * @return bool
68 - */
57 + if (!is_admin()) {
58 + // Gateways are constructed before WordPress finishes parsing the request, so
59 + // is_wc_endpoint_url('order-pay') can't be trusted yet here: defer that check
60 + // to wp_enqueue_scripts, once routing has completed.
61 + if ($this->get_button_checkout()) {
62 + add_action('wp_enqueue_scripts', [$this, 'maybe_add_apple_pay_checkout_assets']);
63 + }
69 64
70 - private function checkApplePay(){
71 - $account = PayplugWoocommerceHelper::get_account_data_from_options();
65 + if ($this->get_button_cart() && !PayplugWoocommerceHelper::is_cart_block() && !PayplugWoocommerceHelper::is_subscription()) {
66 + $this->enabled = 'yes';
67 + $this->add_apple_pay_css();
68 + add_action('woocommerce_proceed_to_checkout', [$this, 'add_apple_pay_cart_js'], 15);
69 + }
72 70
73 - if (isset($account['payment_methods']['apple_pay']['enabled']) ) {
71 + if ($this->get_button_product() && !PayplugWoocommerceHelper::is_product_block()) {
72 + $this->enabled = 'yes';
73 + $this->add_apple_pay_css();
74 + add_action('woocommerce_after_add_to_cart_button', [$this, 'add_apple_pay_product_js'], 15);
75 + }
76 + }
77 + }
78 + }
74 79
75 - if( !empty($account['apple_pay']) && $account['apple_pay'] === 'yes' ) {
76 - $applepay = false;
77 - if ($account['payment_methods']['apple_pay']['enabled']) {
78 - if (in_array($this->domain_name, $account['payment_methods']['apple_pay']['allowed_domain_names'])) {
79 - $applepay = true;
80 - }
81 - }
80 + /**
81 + * Processes admin options for Apple Pay settings.
82 + *
83 + * @return bool|void
84 + */
85 + public function process_admin_options()
86 + {
87 + $data = $this->get_post_data();
88 + if (isset($data['woocommerce_payplug_mode'])) {
89 + if ($this->get_post_data()['woocommerce_payplug_mode'] === '0') {
90 + $options = $this->get_configuration()->get_options();
91 + $options['payment_methods']['configuration']['apple_pay']['active'] = false;
92 + $this->get_configuration()->update_options($options);
93 + }
94 + }
95 + if (isset($data['woocommerce_payplug_apple_pay'])) {
96 + if (($data['woocommerce_payplug_apple_pay'] == 1) && (!$this->checkApplePay())) {
97 + add_action('admin_notices', [$this, 'display_notice']);
98 + }
99 + }
100 + }
82 101
83 - return $applepay;
84 - }
85 - }
102 + /**
103 + * Checks if Apple Pay is authorized and available.
104 + *
105 + * @return bool
106 + */
107 + public function checkApplePay()
108 + {
109 + $options = $this->settings;
86 110
87 - return false;
88 - }
111 + //check if module is enabled
112 + if (!isset($options['enabled']) || !$options['enabled']) {
113 + return false;
114 + }
89 115
90 - /**
91 - * Display unauthorized error
92 - *
93 - * @return void
94 - */
116 + if (!isset($options['payment_methods']) || empty($options['payment_methods'])) {
117 + return false;
118 + }
95 119
96 - public static function display_notice() {
97 - ?>
120 + //it's disabled
121 + if (!(bool) $options['payment_methods']['configuration']['apple_pay']['active']) {
122 + return false;
123 + }
124 +
125 + //Amount validations
126 + if (is_cart() && !empty(WC()->cart)) {
127 + $order_amount = (float) WC()->cart->total;
128 + if ($order_amount < self::MIN_AMOUNT || $order_amount > self::MAX_AMOUNT) {
129 + return false;
130 + }
131 + }
132 +
133 + $display = json_decode($options['payment_methods']['configuration']['apple_pay']['display'], true);
134 + $this->set_button_checkout($display['checkout']);
135 + $this->set_button_cart($display['cart']);
136 + $this->set_button_product($display['product']);
137 +
138 + $carriers = json_decode($options['payment_methods']['configuration']['apple_pay']['carriers'], true);
139 + if (!empty($carriers)) {
140 + $this->set_carriers($carriers);
141 + }
142 +
143 + $account = PayplugWoocommerceHelper::generic_get_account_data_from_options($this->id);
144 + //no auth
145 + if (!isset($account['payment_methods']['apple_pay']) || !isset($account['payment_methods']['apple_pay']['allowed_domain_names'])) {
146 + return false;
147 + }
148 +
149 + //$account has permissions to use apple_pay
150 + $auth = isset($account['payment_methods']['apple_pay']['enabled']) && $account['payment_methods']['apple_pay']['enabled'];
151 + $domain = parse_url(get_site_url());
152 + $auth_domains = in_array($domain['host'], $account['payment_methods']['apple_pay']['allowed_domain_names']);
153 +
154 + //lost auth
155 + if (!($auth && $auth_domains)) {
156 + return false;
157 + }
158 +
159 + return true;
160 + }
161 +
162 + /**
163 + * Outputs the payment fields on the checkout page.
164 + *
165 + * @return void
166 + */
167 + public function payment_fields(): void
168 + {
169 + $description = $this->get_description();
170 +
171 + if (!empty($description)) {
172 + echo wpautop(wptexturize($description));
173 + }
174 + }
175 +
176 + /**
177 + * extend the woocommmerce get description to include personalized html
178 + *
179 + * @return mixed|string|null
180 + */
181 + public function get_description()
182 + {
183 + return apply_filters('woocommerce_gateway_description', $this->description, $this->id);
184 + }
185 +
186 + /**
187 + * Enqueues Apple Pay scripts for the cart page.
188 + *
189 + * @return void
190 + */
191 + public function add_apple_pay_cart_js(): void
192 + {
193 + wp_enqueue_script('apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', [], false, true);
194 + wp_enqueue_script('payplug-apple-pay-cart', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/js/payplug-apple-pay-cart.js', ['jquery', 'apple-pay-sdk'], PAYPLUG_GATEWAY_VERSION, true);
195 + wp_localize_script(
196 + 'payplug-apple-pay-cart',
197 + 'apple_pay_params',
198 + [
199 + 'ajax_url_applepay_get_shippings' => \WC_AJAX::get_endpoint('applepay_get_shippings'),
200 + 'ajax_url_place_order_with_dummy_data' => \WC_AJAX::get_endpoint('place_order_with_dummy_data'),
201 + 'ajax_url_update_applepay_order' => \WC_AJAX::get_endpoint('update_applepay_order'),
202 + 'ajax_url_update_applepay_payment' => \WC_AJAX::get_endpoint('update_applepay_payment'),
203 + 'ajax_url_applepay_get_order_totals' => \WC_AJAX::get_endpoint('applepay_get_order_totals'),
204 + 'ajax_url_applepay_cancel_order' => \WC_AJAX::get_endpoint('applepay_cancel_order'),
205 + 'is_cart' => is_cart(),
206 + 'is_virtual' => !$this->is_shipping_required(),
207 + 'cart_shipping' => WC()->cart->get_shipping_total(),
208 + 'countryCode' => WC()->customer->get_billing_country(),
209 + 'currencyCode' => get_woocommerce_currency(),
210 + 'apple_pay_domain' => $this->domain_name,
211 + ]
212 + );
213 +
214 + if ($this->checkButtonVisibility()) {
215 + echo $this->get_description();
216 + }
217 + }
218 +
219 + /**
220 + * Enqueues Apple Pay scripts for the product page.
221 + *
222 + * @return void
223 + */
224 + public function add_apple_pay_product_js(): void
225 + {
226 + global $product;
227 + // Only dispay ApplePay on product page for simple and variable products
228 + if ($product->get_type() != 'simple' && $product->get_type() != 'variable') {
229 + return;
230 + }
231 + $apple_pay_params = [
232 + 'ajax_url_applepay_get_shippings' => \WC_AJAX::get_endpoint('applepay_get_shippings'),
233 + 'ajax_url_place_order_with_dummy_data' => \WC_AJAX::get_endpoint('place_order_with_dummy_data'),
234 + 'ajax_url_update_applepay_order' => \WC_AJAX::get_endpoint('update_applepay_order'),
235 + 'ajax_url_update_applepay_payment' => \WC_AJAX::get_endpoint('update_applepay_payment'),
236 + 'ajax_url_applepay_get_order_totals' => \WC_AJAX::get_endpoint('applepay_get_order_totals'),
237 + 'ajax_url_applepay_cancel_order' => \WC_AJAX::get_endpoint('applepay_cancel_order'),
238 + 'ajax_url_applepay_empty_cart' => \WC_AJAX::get_endpoint('applepay_empty_cart'),
239 + 'ajax_url_applepay_add_to_cart' => \WC_AJAX::get_endpoint('applepay_add_to_cart'),
240 + 'is_product' => is_product(),
241 + 'is_virtual' => $product->is_virtual(),
242 + 'cart_shipping' => WC()->cart->get_shipping_total(),
243 + 'countryCode' => WC()->customer->get_billing_country(),
244 + 'currencyCode' => get_woocommerce_currency(),
245 + 'apple_pay_domain' => $this->domain_name,
246 + ];
247 + wp_enqueue_script('apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', [], false, true);
248 + wp_enqueue_script('payplug-apple-pay-product', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/js/payplug-apple-pay-product.js', ['jquery', 'apple-pay-sdk'], PAYPLUG_GATEWAY_VERSION, true);
249 + wp_localize_script('payplug-apple-pay-product', 'apple_pay_params', $apple_pay_params);
250 +
251 + if ($this->checkButtonVisibility()) {
252 + echo $this->get_description();
253 + }
254 + }
255 +
256 + /**
257 + * Check if the shipping address is a carrier
258 + *
259 + * @return bool
260 + */
261 + private function checkButtonVisibility()
262 + {
263 + $apple_carriers = $this->get_carriers();
264 + $allowed = false;
265 + $post = $this->get_post_data();
266 + $chosen_method = isset($post['shipping_method'][0]) ? $post['shipping_method'][0] : null;
267 +
268 + if (empty($chosen_method)) {
269 + $chosen_method = !empty(WC()->session->chosen_shipping_methods[0]) ? WC()->session->chosen_shipping_methods[0] : null;
270 + }
271 +
272 + if (!$this->is_shipping_required() || is_product()) {
273 + return true;
274 + }
275 +
276 + foreach (WC()->shipping()->get_packages() as $i => $package) {
277 + $available_rates = !empty($package['rates']) ? $package['rates'] : [];
278 + if (!empty($available_rates)) {
279 + foreach ($available_rates as $method) {
280 + if (in_array($method->get_method_id(), $apple_carriers)) {
281 + // On cart page, show the button if any eligible carrier is available —
282 + // the actual shipping selection happens inside the Apple Pay modal.
283 + // On checkout, restrict to the currently chosen shipping method.
284 + if (is_cart() || $chosen_method === $method->get_method_id() . ':' . $method->get_instance_id()) {
285 + return true;
286 + }
287 + }
288 + }
289 + }
290 + }
291 +
292 + return $allowed;
293 + }
294 +
295 + /**
296 + * check if the shipping is required or not
297 + *
298 + * @return bool
299 + */
300 + public function is_shipping_required()
301 + {
302 + $cart = WC()->cart->get_cart();
303 +
304 + $required = true;
305 + foreach ($cart as $cart_item) {
306 + if (!empty($cart_item['product_id'])) {
307 + $product = wc_get_product($cart_item['product_id']);
308 +
309 + //not required if it enters here
310 + if (!$product->is_virtual() && !$product->is_downloadable()) {
311 + return true;
312 + }
313 +
314 + $required = false;
315 + }
316 + }
317 +
318 + return $required;
319 + }
320 +
321 + /**
322 + * Display unauthorized error
323 + *
324 + * @return void
325 + */
326 + public static function display_notice(): void
327 + {
328 + ?>
98 329 <div class="notice notice-error is-dismissible">
99 - <p><?php echo __( 'payplug_apple_pay_unauthorized_error', 'payplug' ); ?></p>
330 + <p><?php echo __('payplug_apple_pay_unauthorized_error', 'payplug'); ?></p>
100 331 </div>
101 332 <?php
102 - }
333 + }
103 334
104 - /**
105 - * Check User-Agent to make sure it is on Mac OS and in Safari Browser
106 - *
107 - * @return bool
108 - */
109 - private function checkDeviceComptability(){
110 - $user_agent = $_SERVER['HTTP_USER_AGENT'];
335 + /**
336 + * Checks if the current connection is using SSL.
337 + *
338 + * @return bool
339 + */
340 + public function isSSL()
341 + {
342 + if (!empty($_SERVER['HTTPS'])) {
343 + if ('on' == strtolower($_SERVER['HTTPS'])) {
344 + return true;
345 + }
346 + } elseif (isset($_SERVER['SERVER_PORT']) && ('443' == $_SERVER['SERVER_PORT'])) {
347 + return true;
348 + }
111 349
112 - // Check if the Browser is Safari
113 - if (stripos( $user_agent, 'Chrome') !== false) {
114 - return false;
115 - } elseif (stripos( $user_agent, 'Safari') !== false) {
116 - // Check if the OS is Mac
117 - if(!preg_match('/macintosh|mac os x/i', $user_agent)) {
118 - return false;
119 - }
120 - }
350 + if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') {
351 + return true;
352 + }
121 353
122 - return true;
123 - }
354 + return false;
355 + }
124 356
125 - /**
126 - * Check if SSL
127 - *
128 - */
129 - public function isSSL()
130 - {
131 - if( !empty( $_SERVER['HTTPS'] ) ) {
132 - if ( 'on' == strtolower($_SERVER['HTTPS']) )
133 - return true;
134 - } elseif ( isset($_SERVER['SERVER_PORT']) && ( '443' == $_SERVER['SERVER_PORT'] ) ) {
135 - return true;
136 - }
357 + /**
358 + * Enqueues Apple Pay CSS styles.
359 + *
360 + * @return void
361 + */
362 + public function add_apple_pay_css(): void
363 + {
364 + wp_enqueue_style('payplug-apple-pay', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/css/payplug-apple-pay.css', [], PAYPLUG_GATEWAY_VERSION);
365 + }
137 366
138 - if( !empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https' )
139 - return true;
367 + /**
368 + * Enqueues the classic Apple Pay checkout assets, unless the checkout page uses the
369 + * Cart & Checkout blocks (the order-pay page always renders the classic payment form,
370 + * even then, so it still needs the classic assets).
371 + *
372 + * @return void
373 + */
374 + public function maybe_add_apple_pay_checkout_assets(): void
375 + {
376 + if (PayplugWoocommerceHelper::is_checkout_block() && !is_wc_endpoint_url('order-pay')) {
377 + return;
378 + }
140 379
141 - return false;
142 - }
380 + $this->add_apple_pay_css();
381 + $this->add_apple_pay_js();
382 + }
143 383
144 - /**
145 - * Add CSS
146 - *
147 - */
148 - public function add_apple_pay_css() {
149 - wp_enqueue_style('payplug-apple-pay', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/css/payplug-apple-pay.css', [], PAYPLUG_GATEWAY_VERSION);
150 - }
384 + /**
385 + * Enqueues Apple Pay JavaScript for the checkout page.
386 + *
387 + * @return void
388 + */
389 + public function add_apple_pay_js(): void
390 + {
391 + wp_enqueue_script('apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', [], false, true);
392 + wp_enqueue_script(
393 + 'payplug-apple-pay',
394 + PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/js/payplug-apple-pay.js',
395 + [
396 + 'jquery',
397 + 'apple-pay-sdk',
398 + ],
399 + PAYPLUG_GATEWAY_VERSION,
400 + true
401 + );
402 + wp_localize_script(
403 + 'payplug-apple-pay',
404 + 'apple_pay_params',
405 + [
406 + 'ajax_url_payplug_create_order' => \WC_AJAX::get_endpoint('payplug_create_order'),
407 + 'ajax_url_applepay_update_payment' => \WC_AJAX::get_endpoint('applepay_update_payment'),
408 + 'ajax_url_applepay_get_order_totals' => \WC_AJAX::get_endpoint('applepay_get_order_totals'),
409 + 'ajax_url_payplug_apple_pay_create_order_pay' => \WC_AJAX::get_endpoint('payplug_apple_pay_create_order_pay'),
410 + 'countryCode' => WC()->customer->get_billing_country(),
411 + 'currencyCode' => get_woocommerce_currency(),
412 + 'total' => WC()->cart->total,
413 + 'is_checkout' => is_checkout(),
414 + 'is_order_pay' => is_wc_endpoint_url('order-pay'),
415 + 'order_pay_id' => is_wc_endpoint_url('order-pay') ? (int) get_query_var('order-pay') : 0,
416 + 'order_pay_key' => is_wc_endpoint_url('order-pay') ? wc_clean(wp_unslash($_GET['key'] ?? '')) : '',
417 + 'wp_nonce' => wp_create_nonce('woocommerce-process_checkout'),
418 + 'apple_pay_domain' => $this->domain_name,
419 + ]
420 + );
421 + }
151 422
152 - /**
153 - * Add JS
154 - *
155 - */
156 - public function add_apple_pay_js() {
157 - wp_enqueue_script( 'apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js', array(), false, true );
158 - wp_enqueue_script('payplug-apple-pay', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/js/payplug-apple-pay.js',
159 - [
160 - 'jquery',
161 - 'apple-pay-sdk'
162 - ], PAYPLUG_GATEWAY_VERSION, true);
163 - wp_localize_script( 'payplug-apple-pay', 'apple_pay_params',
164 - array(
165 - 'ajax_url_payplug_create_order' => \WC_AJAX::get_endpoint('payplug_create_order'),
166 - 'ajax_url_applepay_update_payment' => \WC_AJAX::get_endpoint('applepay_update_payment'),
167 - 'countryCode' => WC()->customer->get_billing_country(),
168 - 'currencyCode' => get_woocommerce_currency(),
169 - 'total' => WC()->cart->total,
170 - 'apple_pay_domain' => $this->domain_name
171 - )
172 - );
173 - }
423 + /**
424 + * Gets the Apple Pay payment icon HTML.
425 + *
426 + * @return string
427 + */
428 + public function get_icon()
429 + {
430 + $available_img = 'apple-pay-checkout.svg';
431 + $icons = apply_filters('payplug_payment_icons', [
432 + 'payplug' => sprintf('<img src="%s" alt="Apple Pay" class="payplug-payment-icon" />', esc_url(PAYPLUG_GATEWAY_PLUGIN_URL . '/assets/images/checkout/' . $available_img)),
433 + ]);
434 + $icons_str = '';
435 + foreach ($icons as $icon) {
436 + $icons_str .= $icon;
437 + }
174 438
175 - /**
176 - * Get payment icons.
177 - *
178 - * @return string
179 - */
180 - public function get_icon()
181 - {
182 - $available_img = 'apple-pay-checkout.svg';
183 - $icons = apply_filters('payplug_payment_icons', [
184 - 'payplug' => sprintf('<img src="%s" alt="Apple Pay" class="payplug-payment-icon" />', esc_url(PAYPLUG_GATEWAY_PLUGIN_URL . '/assets/images/' . $available_img)),
185 - ]);
186 - $icons_str = '';
187 - foreach ($icons as $icon) {
188 - $icons_str .= $icon;
189 - }
190 - return $icons_str;
191 - }
439 + return $icons_str;
440 + }
192 441
442 + /**
443 + * Processes a payment if it was already generated by an intent.
444 + *
445 + * @param $order
446 + *
447 + * @throws \Exception
448 + *
449 + * @return array|null
450 + */
451 + private function process_standard_intent_payment($order)
452 + {
453 + // This runs from the payplug_apple_pay_create_order_pay AJAX endpoint too, whose own
454 + // request URL never carries the order-pay query var, so is_wc_endpoint_url() alone
455 + // can't detect that context here: fall back to the order_key/order_pay_key sent by
456 + // that endpoint and by the checkout-block create_payment_intent endpoint.
457 + $is_order_pay = $this->is_order_pay_request($order);
193 458
194 - /**
195 - * @param \WC_Order $order
196 - * @param int $amount
197 - * @param int $customer_id
198 - *
199 - * @return array
200 - * @throws \Exception
201 - */
202 - public function process_standard_payment($order, $amount, $customer_id)
203 - {
204 - $order_id = PayplugWoocommerceHelper::is_pre_30() ? $order->id : $order->get_id();
205 - try {
206 - if (!$this->checkApplePay()) {
207 - throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
208 - }
459 + if (!$is_order_pay &&
460 + PayplugWoocommerceHelper::is_checkout_block() &&
461 + !empty($order->get_transaction_id())) {
462 + $order_id = PayplugWoocommerceHelper::is_pre_30() ? $order->id : $order->get_id();
209 463
210 - $address_data = PayplugAddressData::from_order($order);
464 + try {
465 + $payment = $this->payplug_api->payment_retrieve($order->get_transaction_id());
466 + if (ob_get_length() > 0) {
467 + ob_clean();
468 + }
211 469
212 - $return_url = esc_url_raw($order->get_checkout_order_received_url());
470 + $return_url = esc_url_raw($order->get_checkout_order_received_url());
471 + $cancel_url = !empty($payment->hosted_payment->cancel_url) ? $payment->hosted_payment->cancel_url : esc_url_raw(wc_get_checkout_url());
213 472
214 - if (!(substr( $return_url, 0, 4 ) === "http")) {
215 - $return_url = get_site_url().$return_url;
216 - }
473 + // Same payload shape as process_standard_payment(): the frontend's
474 + // BeginSessionFromPaymentDetails() reads merchant_session/cancel_url/return_url
475 + // off of it regardless of which of the two methods produced it.
476 + // payment_method is a write-once attribute (merchant_session is tied to the
477 + // ApplePaySession that created the payment): a retrieved payment may not carry
478 + // it at all, and $payment->payment_method would throw UndefinedAttributeException
479 + // rather than just being null/missing, unlike a plain array access.
480 + $merchant_session = null;
481 + if (isset($payment->payment_method) && is_array($payment->payment_method)) {
482 + $merchant_session = $payment->payment_method['merchant_session'] ?? null;
483 + }
217 484
218 - // delivery_type must be removed in Apple Pay
219 - $billing = $address_data->get_billing();
220 - unset($billing['delivery_type']);
221 - $shipping = $address_data->get_shipping();
222 - unset($shipping['delivery_type']);
485 + if (defined('REST_REQUEST') && REST_REQUEST) {
486 + $merchant_session = wp_json_encode($merchant_session);
487 + }
223 488
224 - $payment_data = [
225 - 'amount' => $amount,
226 - 'currency' => get_woocommerce_currency(),
227 - 'payment_method' => $this->id,
228 - 'payment_context' => array(
229 - 'apple_pay' => array(
230 - 'domain_name' => $this->domain_name,
231 - 'application_data' => base64_encode(json_encode(array(
232 - 'apple_pay_domain' => $this->domain_name,
233 - )))
234 - )
235 - ),
236 - 'billing' => $billing,
237 - 'shipping' => $shipping,
238 - 'hosted_payment' => [
239 - 'return_url' => $return_url,
240 - 'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()),
241 - ],
242 - 'notification_url' => esc_url_raw(WC()->api_request_url('PayplugGateway')),
243 - 'metadata' => [
244 - 'order_id' => $order_id,
245 - 'customer_id' => ((int) $customer_id > 0) ? $customer_id : 'guest',
246 - 'domain' => $this->domain_name,
247 - ]
248 - ];
489 + $result = [
490 + 'result' => 'success',
491 + 'merchant_session' => $merchant_session,
492 + 'payment_id' => $payment->id,
493 + 'cancel_url' => $cancel_url,
494 + 'return_url' => $return_url,
495 + ];
249 496
250 - /**
251 - * Filter the payment data before it's used
252 - *
253 - * @param array $payment_data
254 - * @param int $order_id
255 - * @param array $customer_details
256 - * @param PayplugAddressData $address_data
257 - */
258 - $payment_data = apply_filters('payplug_gateway_payment_data', $payment_data, $order_id, [], $address_data);
259 - $payment = $this->api->payment_create($payment_data);
497 + // wp_send_json_success() calls die(), which is only safe for the classic
498 + // wc-ajax request this was written for: the Store API checkout flow (used by
499 + // the checkout block) calls process_payment() through the REST framework,
500 + // and killing the process mid-request there produces a broken response.
501 + if (wp_doing_ajax()) {
502 + wp_send_json_success($result);
503 + }
260 504
261 - // Save transaction id for the order
262 - PayplugWoocommerceHelper::is_pre_30()
263 - ? update_post_meta($order_id, '_transaction_id', $payment->id)
264 - : $order->set_transaction_id($payment->id);
505 + return $result;
506 + } catch (HttpException $e) {
507 + PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error');
508 + throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
509 + } catch (\Exception $e) {
510 + PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, $e->getMessage()), 'error');
511 + throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
512 + }
513 + }
265 514
266 - if (is_callable([$order, 'save'])) {
267 - $order->save();
268 - }
515 + return null;
516 + }
269 517
270 - /**
271 - * Fires once a payment has been created.
272 - *
273 - * @param int $order_id Order ID
274 - * @param PaymentResource $payment Payment resource
275 - */
276 - \do_action('payplug_gateway_payment_created', $order_id, $payment);
518 + /**
519 + * Processes a standard Apple Pay payment.
520 + *
521 + * @param \WC_Order $order
522 + * @param int $amount
523 + * @param int $customer_id
524 + * @param string $workflow
525 + *
526 + * @throws \Exception
527 + *
528 + * @return array
529 + */
530 + public function process_standard_payment($order, $amount, $customer_id, $workflow = 'checkout')
531 + {
532 + $intent = $this->process_standard_intent_payment($order);
533 + if (!empty($intent)) {
534 + return $intent;
535 + }
277 536
278 - $metadata = PayplugWoocommerceHelper::extract_transaction_metadata($payment);
279 - PayplugWoocommerceHelper::save_transaction_metadata($order, $metadata);
537 + // Same detection as process_standard_intent_payment(): this can run from AJAX
538 + // endpoints whose own request URL never carries the order-pay query var.
539 + $is_order_pay = $this->is_order_pay_request($order);
280 540
281 - PayplugGateway::log(sprintf('Payment creation complete for order #%s', $order_id));
541 + $order_id = PayplugWoocommerceHelper::is_pre_30() ? $order->id : $order->get_id();
542 + try {
543 + $address_data = PayplugAddressData::from_order($order);
282 544
283 - return [
284 - 'result' => 'success',
285 - 'merchant_session' => $payment->payment_method["merchant_session"],
286 - 'payment_id' => $payment->id,
287 - 'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()),
288 - 'return_url' => $return_url,
289 - ];
545 + $return_url = esc_url_raw($order->get_checkout_order_received_url());
290 546
291 - } catch (HttpException $e) {
292 - PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error');
293 - throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
294 - } catch (\Exception $e) {
295 - PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, $e->getMessage()), 'error');
296 - throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
297 - }
547 + if (!(substr($return_url, 0, 4) === 'http')) {
548 + $return_url = get_site_url() . $return_url;
549 + }
298 550
299 - }
551 + // delivery_type must be removed in Apple Pay
552 + $billing = $address_data->get_billing();
553 + unset($billing['delivery_type']);
554 + $shipping = $address_data->get_shipping();
555 + unset($shipping['delivery_type']);
300 556
557 + $payment_data = [
558 + 'amount' => $amount,
559 + 'currency' => get_woocommerce_currency(),
560 + 'payment_method' => $this->id,
561 + 'payment_context' => [
562 + 'apple_pay' => [
563 + 'domain_name' => $this->domain_name,
564 + 'application_data' => base64_encode(json_encode([
565 + 'apple_pay_domain' => $this->domain_name,
566 + ])),
567 + ],
568 + ],
569 + 'billing' => $billing,
570 + 'shipping' => $shipping,
571 + 'hosted_payment' => [
572 + 'return_url' => $return_url,
573 + 'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()),
574 + ],
575 + 'notification_url' => esc_url_raw(WC()->api_request_url('PayplugGateway')),
576 + 'metadata' => [
577 + 'order_id' => $order_id,
578 + 'customer_id' => ((int) $customer_id > 0) ? $customer_id : 'guest',
579 + 'domain' => $this->domain_name,
580 + 'applepay_workflow' => $workflow,
581 + ],
582 + ];
583 +
584 + if (PayplugWoocommerceHelper::is_checkout_block() && is_checkout()) {
585 + $payment_data['metadata']['woocommerce_block'] = 'CHECKOUT';
586 + } elseif (PayplugWoocommerceHelper::is_cart_block() && is_cart()) {
587 + $payment_data['metadata']['woocommerce_block'] = 'CART';
588 + }
589 +
590 + /**
591 + * Filter the payment data before it's used
592 + *
593 + * @param array $payment_data
594 + * @param int $order_id
595 + * @param array $customer_details
596 + * @param PayplugAddressData $address_data
597 + */
598 + $payment_data = apply_filters('payplug_gateway_payment_data', $payment_data, $order_id, [], $address_data);
599 + $payment = $this->payplug_api->payment_create($payment_data);
600 +
601 + // Save transaction id for the order
602 + PayplugWoocommerceHelper::is_pre_30()
603 + ? update_post_meta($order_id, '_transaction_id', $payment->id)
604 + : $order->set_transaction_id($payment->id);
605 +
606 + if (is_callable([$order, 'save'])) {
607 + $order->save();
608 + }
609 +
610 + /**
611 + * Fires once a payment has been created.
612 + *
613 + * @param int $order_id Order ID
614 + * @param PaymentResource $payment Payment resource
615 + */
616 + \do_action('payplug_gateway_payment_created', $order_id, $payment);
617 +
618 + $metadata = PayplugWoocommerceHelper::extract_transaction_metadata($payment);
619 + PayplugWoocommerceHelper::save_transaction_metadata($order, $metadata);
620 +
621 + PayplugGateway::log(sprintf('Payment creation complete for order #%s', $order_id));
622 +
623 + // On order-pay, closing the Apple Pay sheet should keep the customer on the
624 + // order-pay page. On a regular checkout submission (classic or Blocks), it should
625 + // keep them on checkout too, so they can pick another payment method - only the
626 + // classic cart/product page flows (workflow 'cart'/'product') actually want the
627 + // order-cancelled/cart redirect, since that's where those customers started.
628 + if ($is_order_pay) {
629 + $cancel_url = esc_url_raw($order->get_checkout_payment_url());
630 + } elseif ('checkout' === $workflow) {
631 + $cancel_url = esc_url_raw(wc_get_checkout_url());
632 + } else {
633 + $cancel_url = esc_url_raw($order->get_cancel_order_url_raw());
634 + }
635 +
636 + // When process_payment() is invoked through WooCommerce Blocks' Store API (a REST
637 + // request), this array is forwarded to the client as `payment_details`, which
638 + // coerces every value to a string - an array value would become the literal,
639 + // useless string "Array". The classic AJAX flows that call this method directly
640 + // (order-pay, cart/product Apple Pay) JSON-encode/decode the whole response
641 + // transparently instead, so they need the raw merchant session object.
642 + $merchant_session = $payment->payment_method['merchant_session'];
643 + if (defined('REST_REQUEST') && REST_REQUEST) {
644 + $merchant_session = wp_json_encode($merchant_session);
645 + }
646 +
647 + return [
648 + 'result' => 'success',
649 + 'merchant_session' => $merchant_session,
650 + 'payment_id' => $payment->id,
651 + 'cancel_url' => $cancel_url,
652 + 'return_url' => $return_url,
653 + ];
654 + } catch (HttpException $e) {
655 + PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error');
656 + if ($workflow === 'cart') {
657 + wp_send_json_error([
658 + 'code' => $e->getCode(),
659 + 'msg' => __('Payment processing failed. Please retry.', 'payplug'),
660 + 'order_id' => $order_id,
661 + ]);
662 + }
663 + throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
664 + } catch (\Exception $e) {
665 + PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, $e->getMessage()), 'error');
666 + if ($workflow === 'cart') {
667 + wp_send_json_error([
668 + 'code' => $e->getCode(),
669 + 'msg' => __('Payment processing failed. Please retry.', 'payplug'),
670 + 'order_id' => $order_id,
671 + ]);
672 + }
673 + throw new \Exception(__('Payment processing failed. Please retry.', 'payplug'));
674 + }
675 + }
676 +
677 + /**
678 + * Sets the checkout button status.
679 + *
680 + * @param bool $status
681 + *
682 + * @return void
683 + */
684 + private function set_button_checkout($status): void
685 + {
686 + $this->checkout = $status;
687 + }
688 +
689 + /**
690 + * Sets the cart button status.
691 + *
692 + * @param bool $status
693 + *
694 + * @return void
695 + */
696 + private function set_button_cart($status): void
697 + {
698 + $this->cart = $status;
699 + }
700 +
701 + /**
702 + * Sets the product button status.
703 + *
704 + * @param bool $status
705 + *
706 + * @return void
707 + */
708 + private function set_button_product($status): void
709 + {
710 + $this->product = $status;
711 + }
712 +
713 + /**
714 + * Gets the checkout button status.
715 + *
716 + * @return bool
717 + */
718 + private function get_button_checkout()
719 + {
720 + return $this->checkout;
721 + }
722 +
723 + /**
724 + * Gets the cart button status.
725 + *
726 + * @return bool
727 + */
728 + public function get_button_cart()
729 + {
730 + return $this->cart;
731 + }
732 +
733 + /**
734 + * Gets the product button status.
735 + *
736 + * @return bool
737 + */
738 + public function get_button_product()
739 + {
740 + return $this->product;
741 + }
742 +
743 + /**
744 + * Gets the list of allowed carriers for Apple Pay.
745 + *
746 + * @return array
747 + */
748 + public function get_carriers()
749 + {
750 + return $this->carriers;
751 + }
752 +
753 + /**
754 + * Sets the list of allowed carriers for Apple Pay.
755 + *
756 + * @param array $carriers
757 + *
758 + * @return void
759 + */
760 + private function set_carriers($carriers): void
761 + {
762 + $this->carriers = $carriers;
763 + }
301 764 }