PluginProbe ʕ •ᴥ•ʔ
WooCommerce Square / 5.4.2
WooCommerce Square v5.4.2
5.4.2 5.4.1 5.4.0 trunk 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.7.0 2.8.0 2.9.0 2.9.1 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.2.0 3.3.0 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.7.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 4.0.0 4.1.0 4.2.0 4.2.1 4.2.2 4.2.3 4.3.0 4.3.1 4.3.2 4.4.0 4.4.1 4.4.2 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.9.0 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 4.9.6 4.9.7 4.9.8 4.9.9 5.0.0 5.0.1 5.1.0 5.1.1 5.1.2 5.2.0 5.3.0 5.3.1 5.3.2 5.3.3
woocommerce-square / includes / Gateway / Digital_Wallet.php
woocommerce-square / includes / Gateway Last commit date
API 4 months ago Templates 1 year ago API.php 2 months ago Blocks_Handler.php 3 months ago Card_Handler.php 3 years ago Cash_App_Pay_Blocks_Handler.php 3 months ago Cash_App_Pay_Gateway.php 3 months ago Customer_Helper.php 3 years ago Digital_Wallet.php 3 months ago Gift_Card.php 3 months ago Payment_Form.php 3 months ago
Digital_Wallet.php
1458 lines
1 <?php
2 /**
3 * WooCommerce Square
4 *
5 * This source file is subject to the GNU General Public License v3.0
6 * that is bundled with this package in the file license.txt.
7 * It is also available through the world-wide-web at this URL:
8 * http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
9 * If you did not receive a copy of the license and are unable to
10 * obtain it through the world-wide-web, please send an email
11 * to license@woocommerce.com so we can send you a copy immediately.
12 *
13 * DISCLAIMER
14 *
15 * Do not edit or add to this file if you wish to upgrade WooCommerce Square to newer
16 * versions in the future. If you wish to customize WooCommerce Square for your
17 * needs please refer to https://docs.woocommerce.com/document/woocommerce-square/
18 */
19
20 namespace WooCommerce\Square\Gateway;
21
22 defined( 'ABSPATH' ) || exit;
23
24 use WooCommerce\Square\Plugin;
25 use WooCommerce\Square\Utilities\Order_Ajax_Authorization;
26
27 class Digital_Wallet {
28
29 /**
30 * @var Gateway $gateway
31 */
32 public $gateway = null;
33
34 /**
35 * @var String $page - Current page
36 */
37 public $page = null;
38
39 /**
40 * @var bool $is_available - Is Apple Pay and Google Pay available
41 */
42 public $is_available = null;
43
44 /**
45 * @var string just a simple text, 'via WooCommerce'.
46 */
47 public $total_label_suffix;
48
49 /**
50 * @var array Array of localised data.
51 */
52 protected $localised_data = array();
53
54 /**
55 * Setup the Digital Wallet class
56 *
57 * @since 2.3
58 */
59 public function __construct( $gateway ) {
60 $this->gateway = $gateway;
61
62 if ( 'yes' === $gateway->get_option( 'enabled', 'no' ) && $this->is_digital_wallet_enabled() ) {
63 add_action( 'wp', array( $this, 'init' ) );
64 add_action( 'admin_notices', array( $this, 'admin_notices' ) );
65 }
66
67 if ( is_admin() && ( $gateway->get_plugin()->is_gateway_settings() || $gateway->get_plugin()->is_plugin_settings() ) ) {
68 add_action( 'init', array( $this, 'apple_pay_domain_registration' ), 11 );
69 }
70
71 // WC AJAX
72 add_action( 'wc_ajax_square_digital_wallet_get_payment_request', array( $this, 'ajax_get_payment_request' ) );
73 add_action( 'wc_ajax_square_digital_wallet_add_to_cart', array( $this, 'ajax_add_to_cart' ) );
74 add_action( 'wc_ajax_square_digital_wallet_recalculate_totals', array( $this, 'ajax_recalculate_totals' ) );
75 add_action( 'wc_ajax_square_digital_wallet_process_checkout', array( $this, 'ajax_process_checkout' ) );
76
77 // Calculate the value of option `wc_square_apple_pay_enabled` which is not stored in the DB for WC Admin inbox notifications
78 add_filter( 'pre_option_wc_square_apple_pay_enabled', array( $this, 'get_option_is_apple_pay_enabled' ), 10, 1 );
79 add_filter( 'woocommerce_checkout_posted_data', array( $this, 'filter_posted_data' ) );
80 add_filter( 'woocommerce_checkout_fields', array( $this, 'filter_checkout_fields' ) );
81 }
82
83 /**
84 * Initialize the digital wallet class
85 *
86 * @since 2.3
87 */
88 public function init() {
89
90 /**
91 * Filter to update the 'via WooCommerce' text.
92 *
93 * 'woocommerce_square_payment_request_total_label_suffix' is the filter hook.
94 * 'via WooCommerce' is the value being filtered.
95 *
96 * @since 3.2
97 */
98 $total_label_suffix = apply_filters( 'woocommerce_square_payment_request_total_label_suffix', __( 'via WooCommerce', 'woocommerce-square' ) );
99 $this->total_label_suffix = $total_label_suffix ? " ($total_label_suffix)" : '';
100
101 $is_user_logged_in = is_user_logged_in();
102 $is_registration_required = WC()->checkout->is_registration_required();
103 $is_registration_enabled = WC()->checkout->is_registration_enabled();
104
105 $available_pages = $this->get_available_pages();
106
107 if ( ( $is_user_logged_in || ! $is_registration_required ) && in_array( 'product', $available_pages, true ) ) {
108 add_action( 'woocommerce_after_add_to_cart_button', array( $this, 'render_button' ) );
109 }
110
111 if ( ( $is_user_logged_in || ! $is_registration_required ) && in_array( 'cart', $available_pages, true ) ) {
112 /*
113 * Add Express Pay buttons to cart page.
114 *
115 * This is registered to run late (at priority 20) to ensure the buttons are
116 * added following the default WooCommerce proceed to checkout button.
117 */
118 add_action( 'woocommerce_proceed_to_checkout', array( $this, 'render_button' ), 20 );
119 }
120
121 if ( ( $is_user_logged_in || ! $is_registration_required || $is_registration_enabled ) && in_array( 'checkout', $available_pages, true ) ) {
122 add_action( 'woocommerce_before_checkout_form', array( $this, 'render_button' ), 15 );
123 }
124
125 if ( ( $is_user_logged_in || ! $is_registration_required || $is_registration_enabled ) && in_array( 'checkout', $available_pages, true ) && is_wc_endpoint_url( 'order-pay' ) ) {
126 add_action( 'before_woocommerce_pay', array( $this, 'render_button' ) );
127 }
128
129 $page = $this->get_current_page();
130 $payment_request = false;
131
132 try {
133 $payment_request = $this->get_payment_request_for_context( $page );
134 } catch ( \Exception $e ) {
135 $this->gateway->get_plugin()->log( 'Error: ' . $e->getMessage() );
136 }
137
138 if ( $this->gateway && $page ) {
139 $this->localised_data = array(
140 'application_id' => $this->gateway->get_application_id(),
141 'location_id' => wc_square()->get_settings_handler()->get_location_id(),
142 'gateway_id' => $this->gateway->get_id(),
143 'gateway_id_dasherized' => $this->gateway->get_id_dasherized(),
144 'payment_request' => $payment_request,
145 'context' => $page,
146 'general_error' => __( 'An error occurred, please try again or try an alternate form of payment.', 'woocommerce-square' ),
147 'ajax_url' => \WC_AJAX::get_endpoint( '%%endpoint%%' ),
148 'payment_request_nonce' => wp_create_nonce( 'wc-square-get-payment-request' ),
149 'add_to_cart_nonce' => wp_create_nonce( 'wc-square-add-to-cart' ),
150 'recalculate_totals_nonce' => wp_create_nonce( 'wc-square-recalculate-totals' ),
151 'process_checkout_nonce' => wp_create_nonce( 'woocommerce-process_checkout' ),
152 'logging_enabled' => $this->gateway->debug_log(),
153 'hide_button_options' => $this->get_hidden_button_options(),
154 'google_pay_color' => $this->gateway->get_option( 'digital_wallets_google_pay_button_color', 'black' ),
155 'apple_pay_color' => $this->gateway->get_option( 'digital_wallets_apple_pay_button_color', 'black' ),
156 'apple_pay_type' => $this->gateway->get_option( 'digital_wallets_button_type', 'buy' ),
157 'buy_with_gpay_text' => __( 'Buy with GPay', 'woocommerce-square' ),
158 'opens_in_new_window_text' => __( 'opens in a new window', 'woocommerce-square' ),
159 'is_pay_for_order_page' => is_checkout() && is_wc_endpoint_url( 'order-pay' ),
160 'order_id' => absint( get_query_var( 'order-pay' ) ),
161 'order_key' => Order_Ajax_Authorization::get_order_key_for_frontend_localization(),
162 );
163 }
164
165 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
166 }
167
168 public function get_localised_data() {
169 return $this->localised_data;
170 }
171
172 /**
173 * Display admin notices related to Square digital wallets
174 *
175 * @since 2.3
176 * @return void
177 */
178 public function admin_notices() {
179
180 // Apple Pay notices - Only shown when digital wallets are enabled and Apple isn't in list of hidden button options
181 if ( ! in_array( 'apple', $this->gateway->get_option( 'digital_wallets_hide_button_options', array() ), true ) ) {
182 $apple_pay_verification_file_location = $this->apple_pay_verification_file_location();
183
184 // Verification file is missing
185 if ( ! empty( $apple_pay_verification_file_location ) && ! file_exists( $apple_pay_verification_file_location ) ) {
186 wc_square()->get_admin_notice_handler()->add_admin_notice(
187 sprintf(
188 /* Translators: %1$s: expected location of apple pay verification file, %2$s: opening href tag with link to Square documentation, %3$s: closing href tag */
189 __( 'Apple Pay is not available with Square. We cannot confirm the Apple Pay domain verification file is at the expected location: %1$s. For more information, please read our documentation on %2$sSetting up Apple Pay%3$s.', 'woocommerce-square' ),
190 '<code>' . $apple_pay_verification_file_location . '</code>',
191 '<a href="https://docs.woocommerce.com/document/woocommerce-square/">',
192 '</a>'
193 ),
194 'wc-square-apple-pay-file-missing',
195 array(
196 'notice_class' => 'notice-warning',
197 )
198 );
199
200 } elseif ( 'no' === $this->gateway->get_option( 'apple_pay_domain_registered', '' ) && 'yes' === $this->gateway->get_option( 'apple_pay_domain_registration_attempted', 'no' ) ) {
201 // Domain failed to register
202 wc_square()->get_admin_notice_handler()->add_admin_notice(
203 sprintf(
204 /* Translators: %1$s: opening bold tags, %2$s: closing strong/bold tags, %3$s: expected location of apple pay verification file, %4$s: opening href tag with link to Square documentation, %5$s: closing href tag */
205 __( 'Apple Pay is not available with Square - there was a problem with registering your store domain with Square/Apple Pay. %1$sView the Square logs%2$s to find out what caused the registration to fail.', 'woocommerce-square' ),
206 '<a href="' . esc_url( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) . '">',
207 '</a>'
208 ),
209 'wc-square-apple-pay-domain-registered',
210 array(
211 'notice_class' => 'notice-warning',
212 )
213 );
214 }
215 }
216 }
217
218 /**
219 * Returns true if the checkout using Digital Wallets require custom fields
220 * to not be empty.
221 *
222 * @since 4.3.1
223 *
224 * @return boolean
225 */
226 public function does_checkout_require_custom_fields() {
227 // Default WooCommerce Core required fields for billing, shipping, account and order.
228 $default_required_fields = array(
229 'billing_first_name',
230 'billing_last_name',
231 'billing_company',
232 'billing_country',
233 'billing_address_1',
234 'billing_address_2',
235 'billing_city',
236 'billing_state',
237 'billing_postcode',
238 'billing_phone',
239 'billing_email',
240 'shipping_first_name',
241 'shipping_last_name',
242 'shipping_company',
243 'shipping_country',
244 'shipping_address_1',
245 'shipping_address_2',
246 'shipping_city',
247 'shipping_state',
248 'shipping_postcode',
249 'order_comments',
250 'account_username',
251 'account_password',
252 );
253
254 $fields = WC()->checkout()->get_checkout_fields();
255 $fields = array_merge(
256 $fields['billing'] ?? array(),
257 $fields['shipping'] ?? array(),
258 $fields['order'] ?? array(),
259 $fields['account'] ?? array(),
260 );
261
262 foreach ( $fields as $field_key => $field_data ) {
263 if ( false === array_search( $field_key, $default_required_fields, true ) ) {
264 if ( isset( $field_data['required'] ) && true === $field_data['required'] ) {
265 return true;
266 }
267 }
268 }
269
270 return false;
271 }
272
273 /**
274 * Render the Digital Wallet buttons (Apple Pay) on the Product, Cart or Checkout pages
275 *
276 * @since 2.3
277 * @return void
278 */
279 public function render_button() {
280
281 if ( self::does_checkout_require_custom_fields() && ! is_checkout() ) {
282 return;
283 }
284
285 $apple_pay_classes = $google_pay_classes = array( 'wc-square-wallet-buttons' );
286 $button_type = $this->gateway->get_option( 'digital_wallets_button_type', 'buy' );
287 $apple_button_style = $this->gateway->get_option( 'digital_wallets_apple_pay_button_color', 'black' );
288
289 // set button text
290 switch ( $button_type ) {
291 case 'donate':
292 case 'buy':
293 $button_text = ucfirst( $button_type ) . ' with';
294 $apple_pay_classes[] = 'wc-square-wallet-button-with-text';
295 break;
296
297 default:
298 $button_text = '';
299 }
300
301 $apple_pay_classes[] = 'wc-square-wallet-button-' . $apple_button_style;
302
303 ?>
304 <div id="wc-square-digital-wallet" style="display:none;">
305 <div id="apple-pay-button" class="apple-pay-button <?php echo esc_attr( implode( ' ', array_map( 'sanitize_html_class', $apple_pay_classes ) ) ); ?>" lang="<?php echo esc_attr( substr( get_locale(), 0, 2 ) ); ?>" style="-apple-pay-button-type: <?php echo esc_attr( $button_type ); ?>; -apple-pay-button-style: <?php echo esc_attr( $apple_button_style ); ?>">
306 <span class="text"><?php echo esc_html( $button_text ); ?></span>
307 <span class="logo"></span>
308 </div>
309
310 <div id="wc-square-google-pay" lang="<?php echo esc_attr( substr( get_locale(), 0, 2 ) ); ?>"></div>
311
312 <?php
313 /**
314 * Filter whether to show the divider between the Square digital wallet buttons and the checkout.
315 *
316 * This filter allows extensions to hide the "-- OR --" divider between express pay/wallet buttons
317 * and the checkout. This is useful for plugins adding their own express pay/wallet buttons following
318 * the Square buttons.
319 *
320 * @since 4.4.1
321 *
322 * @param bool $show_divider Whether to show the divider. Default true.
323 */
324 $show_divider = apply_filters( 'wp_square_show_digital_wallet_divider_on_checkout', true );
325 if ( $show_divider && is_checkout() ) :
326 ?>
327 <p id="wc-square-wallet-divider">&ndash; <?php esc_html_e( 'OR', 'woocommerce-square' ); ?> &ndash;</p>
328 <?php
329 endif;
330 ?>
331 </div>
332 <?php
333 }
334
335 /**
336 * Load Square wallet scripts and styles
337 *
338 * @since 2.3
339 * @return void
340 */
341 public function enqueue_scripts() {
342 $page = $this->get_current_page();
343
344 if ( ! $page || ! $this->is_available() ) {
345 return;
346 }
347
348 wp_enqueue_style( 'wc-square-digital-wallet', $this->gateway->get_plugin()->get_plugin_url() . '/build/assets/frontend/wc-square-digital-wallet.css', array(), Plugin::VERSION );
349 wp_enqueue_script( 'wc-square-digital-wallet', $this->gateway->get_plugin()->get_plugin_url() . '/build/assets/frontend/wc-square-digital-wallet.js', array( 'jquery' ), Plugin::VERSION, true );
350
351 try {
352 /**
353 * Hook to filter localized data for digital wallets.
354 *
355 * @param array Array of data for digital wallets.
356 * @since 2.3
357 */
358 $args = apply_filters(
359 'wc_square_digital_wallet_js_args',
360 $this->get_localised_data()
361 );
362
363 ob_start();
364 ?>
365 jQuery(function($) {
366 window.wc_square_digital_wallet_handler = new WC_Square_Digital_Wallet_Handler( <?php echo wp_json_encode( $args ); ?> );
367 } );
368 <?php
369 $javascript = ob_get_clean();
370 \WooCommerce\Square\Utilities\Helper::enqueue_inline_script( 'wc-square-digital-wallet-inline', $javascript, array( 'jquery', 'wc-square-digital-wallet' ) );
371 } catch ( \Exception $e ) {
372 wp_dequeue_style( 'wc-square-digital-wallet' );
373 wp_dequeue_script( 'wc-square-digital-wallet' );
374 }
375 }
376
377 /**
378 * Build the payment request object for the given context (i.e. product, cart or checkout page)
379 *
380 * Payment request objects are used by the Payments and need to be in a specific format.
381 * Reference: https://developer.squareup.com/docs/api/paymentform#paymentform-paymentrequestobjects
382 *
383 * @since 2.3
384 * @param string $context
385 * @return array
386 */
387 public function get_payment_request_for_context( $context ) {
388 // Ignoring nonce verification checks as it is already handled in the parent function.
389 $payment_request = array();
390 $is_pay_for_order_page = isset( $_POST['is_pay_for_order_page'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['is_pay_for_order_page'] ) ) : is_wc_endpoint_url( 'order-pay' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
391 $order_id = isset( $_POST['order_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : absint( get_query_var( 'order-pay' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
392
393 switch ( $context ) {
394 case 'product':
395 try {
396 $payment_request = $this->get_product_payment_request( get_the_ID() );
397 } catch ( \Exception $e ) {
398 $this->gateway->get_plugin()->log( 'Error: ' . $e->getMessage() );
399 }
400 break;
401
402 case 'cart':
403 case 'checkout':
404 if ( is_wc_endpoint_url( 'order-pay' ) || $is_pay_for_order_page ) {
405 $order = wc_get_order( $order_id );
406
407 if ( ! Order_Ajax_Authorization::is_authorized_for_pay_for_order( $order ) ) {
408 throw new \Exception( Order_Ajax_Authorization::get_invalid_order_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- already escaped in get_invalid_order_message()
409 }
410
411 $payment_request = $this->build_payment_request(
412 $order->get_total(),
413 array(
414 'order_id' => $order_id,
415 'is_pay_for_order_page' => $is_pay_for_order_page,
416 )
417 );
418 } elseif ( isset( WC()->cart ) && $this->allowed_for_cart() ) {
419 WC()->cart->calculate_totals();
420 $payment_request = $this->build_payment_request( WC()->cart->total );
421 }
422
423 break;
424 }
425
426 return $payment_request;
427 }
428
429 /**
430 * Build a payment request object to be sent to Payments on the product page
431 *
432 * Documentation: https://developer.squareup.com/docs/api/paymentform#paymentform-paymentrequestobjects
433 *
434 * @since 2.3
435 * @param int $product_id
436 * @param bool $add_to_cart - whether or not the product needs to be added to the cart before building the payment request
437 * @return array
438 */
439 public function get_product_payment_request( $product_id = 0, $quantity = 1, $attributes = array(), $add_to_cart = false ) {
440 $data = array();
441 $items = array();
442 $product_id = ! empty( $product_id ) ? $product_id : get_the_ID();
443 $product = wc_get_product( $product_id );
444 $variation_id = 0;
445
446 if ( ! is_a( $product, 'WC_Product' ) ) {
447 /* translators: product ID */
448 throw new \Exception( sprintf( esc_html__( 'Product with the ID (%d) cannot be found.', 'woocommerce-square' ), absint( $product_id ) ) );
449 }
450
451 $quantity = $product->is_sold_individually() ? 1 : $quantity;
452
453 if ( 'variable' === $product->get_type() && ! empty( $attributes ) ) {
454 $data_store = \WC_Data_Store::load( 'product' );
455 $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
456
457 if ( ! empty( $variation_id ) ) {
458 $product = wc_get_product( $variation_id );
459 }
460 }
461
462 if ( ! $product->has_enough_stock( $quantity ) ) {
463 /* translators: 1: product name 2: quantity in stock */
464 throw new \Exception( sprintf( esc_html__( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-square' ), esc_html( $product->get_name() ), esc_html( wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) ) );
465 }
466
467 if ( ! $product->is_purchasable() ) {
468 /* translators: 1: product name */
469 throw new \Exception( sprintf( esc_html__( 'You cannot purchase "%1$s" because it is currently not available.', 'woocommerce-square' ), esc_html( $product->get_name() ) ) );
470 }
471
472 if ( $add_to_cart ) {
473 WC()->cart->empty_cart();
474 WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes );
475
476 WC()->cart->calculate_totals();
477 return $this->build_payment_request( WC()->cart->total );
478 }
479
480 $product_price = (float) $product->get_price();
481 $amount = number_format( $quantity * $product_price, 2, '.', '' );
482 $quantity_label = 1 < $quantity ? ' x ' . $quantity : '';
483
484 $items[] = array(
485 'label' => $product->get_name() . $quantity_label,
486 'amount' => $amount,
487 'pending' => false,
488 );
489
490 if ( wc_tax_enabled() ) {
491 $items[] = array(
492 'label' => __( 'Tax', 'woocommerce-square' ),
493 'amount' => '0.00',
494 'pending' => false,
495 );
496 }
497
498 $data['requestShippingContact'] = $product->needs_shipping();
499 $data['lineItems'] = $items;
500
501 return $this->build_payment_request( $amount, $data );
502 }
503
504 /**
505 * Build a payment request object to be sent to Payments.
506 *
507 * Documentation: https://developer.squareup.com/docs/api/paymentform#paymentform-paymentrequestobjects
508 *
509 * @since 2.3
510 * @param string $amount - format '100.00'
511 * @param array $data
512 * @return array
513 */
514 public function build_payment_request( $amount, $data = array() ) {
515 $is_pay_for_order_page = isset( $data['is_pay_for_order_page'] ) ? $data['is_pay_for_order_page'] : false;
516 $order_id = isset( $data['order_id'] ) ? $data['order_id'] : 0;
517
518 if ( $is_pay_for_order_page ) {
519 $request_shipping_contact = false;
520 } else {
521 $request_shipping_contact = isset( WC()->cart ) && WC()->cart->needs_shipping();
522 }
523
524 $order_data = array();
525 $data = wp_parse_args(
526 $data,
527 array(
528 'requestShippingContact' => $request_shipping_contact,
529 'requestEmailAddress' => true,
530 'requestBillingContact' => true,
531 'countryCode' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ),
532 'currencyCode' => get_woocommerce_currency(),
533 )
534 );
535
536 if ( $is_pay_for_order_page ) {
537 $order = wc_get_order( $order_id );
538 $order_data = array(
539 'subtotal' => $order->get_subtotal(),
540 'discount' => $order->get_discount_total(),
541 'shipping' => $order->get_shipping_total(),
542 'fees' => $order->get_total_fees(),
543 'taxes' => $order->get_total_tax(),
544 );
545
546 unset( $data['is_pay_for_order_page'], $data['order_id'] );
547 }
548
549 if ( count( WC()->shipping->get_packages() ) > 1 ) {
550 throw new \Exception( esc_html__( 'This payment method cannot be used for multiple shipments.', 'woocommerce-square' ) );
551 }
552
553 if ( ! isset( $data['lineItems'] ) ) {
554 $data['lineItems'] = $this->build_payment_request_line_items( $order_data );
555 }
556
557 if ( true === $data['requestShippingContact'] ) {
558 $data['shippingOptions'] = array(
559 array(
560 'id' => '0',
561 'label' => __( 'Pending', 'woocommerce-square' ),
562 'amount' => '0.00',
563 'pending' => false,
564 ),
565 );
566 }
567
568 $data['total'] = array(
569 'label' => get_bloginfo( 'name', 'display' ) . esc_html( $this->total_label_suffix ),
570 'amount' => number_format( $amount, 2, '.', '' ),
571 'pending' => false,
572 );
573
574 return $data;
575 }
576
577 /**
578 * Builds an array of line items/totals to be sent back to Square in the lineItems array.
579 *
580 * @since 2.3
581 * @param array $totals
582 * @return array
583 */
584 public function build_payment_request_line_items( $totals = array() ) {
585 // Ignoring nonce verification checks as it is already handled in the parent function.
586 $totals = empty( $totals ) ? $this->get_cart_totals() : $totals;
587 $line_items = array();
588 $order_id = isset( $_POST['order_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : absint( get_query_var( 'order-pay' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
589 $order_id = Order_Ajax_Authorization::trusted_line_items_order_id( $order_id );
590
591 if ( $order_id ) {
592 $order = wc_get_order( $order_id );
593 $iterable = $order->get_items();
594 } else {
595 $iterable = WC()->cart->get_cart();
596 }
597
598 foreach ( $iterable as $item ) {
599 $amount = number_format( $order_id ? $order->get_subtotal() : $item['line_subtotal'], 2, '.', '' );
600
601 if ( $order_id ) {
602 $quantity_label = 1 < $item->get_quantity() ? ' x ' . $item->get_quantity() : '';
603 } else {
604 $quantity_label = 1 < $item['quantity'] ? ' x ' . $item['quantity'] : '';
605 }
606
607 $item = array(
608 'label' => $order_id ? $item->get_name() . $quantity_label : $item['data']->get_name() . $quantity_label,
609 'amount' => $amount,
610 'pending' => false,
611 );
612
613 $line_items[] = $item;
614 }
615
616 if ( $totals['shipping'] > 0 ) {
617 $line_items[] = array(
618 'label' => __( 'Shipping', 'woocommerce-square' ),
619 'amount' => number_format( $totals['shipping'], 2, '.', '' ),
620 'pending' => false,
621 );
622 }
623
624 if ( $totals['taxes'] > 0 ) {
625 $line_items[] = array(
626 'label' => __( 'Tax', 'woocommerce-square' ),
627 'amount' => number_format( $totals['taxes'], 2, '.', '' ),
628 'pending' => false,
629 );
630 }
631
632 if ( $totals['discount'] > 0 ) {
633 $line_items[] = array(
634 'label' => __( 'Discount', 'woocommerce-square' ),
635 'amount' => number_format( $totals['discount'], 2, '.', '' ),
636 'pending' => false,
637 );
638 }
639
640 if ( $totals['fees'] > 0 ) {
641 $line_items[] = array(
642 'label' => __( 'Fees', 'woocommerce-square' ),
643 'amount' => number_format( $totals['fees'], 2, '.', '' ),
644 'pending' => false,
645 );
646 }
647
648 return $line_items;
649 }
650
651 /**
652 * Get the payment request object in an ajax request
653 *
654 * @since 2.3
655 * @return void
656 */
657 public function ajax_get_payment_request() {
658 check_ajax_referer( 'wc-square-get-payment-request', 'security' );
659
660 $payment_request = array();
661 $context = ! empty( $_POST['context'] ) ? wc_clean( wp_unslash( $_POST['context'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
662
663 try {
664 if ( 'product' === $context ) {
665 $product_id = ! empty( $_POST['product_id'] ) ? wc_clean( wp_unslash( $_POST['product_id'] ) ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
666 $quantity = ! empty( $_POST['quantity'] ) ? wc_clean( wp_unslash( $_POST['quantity'] ) ) : 1; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
667 $attributes = ! empty( $_POST['attributes'] ) ? wc_clean( wp_unslash( $_POST['attributes'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
668
669 try {
670 $payment_request = $this->get_product_payment_request( $product_id, $quantity, $attributes );
671 } catch ( \Exception $e ) {
672 wp_send_json_error( $e->getMessage() );
673 }
674 } else {
675 $payment_request = $this->get_payment_request_for_context( $context );
676 }
677
678 if ( empty( $payment_request ) ) {
679 /* translators: Context (product, cart, checkout or page) */
680 throw new \Exception( sprintf( esc_html__( 'Empty payment request data for %s.', 'woocommerce-square' ), ! empty( $context ) ? $context : 'page' ) );
681 }
682 } catch ( \Exception $e ) {
683 wp_send_json_error( $e->getMessage() );
684 }
685
686 wp_send_json_success( wp_json_encode( $payment_request ) );
687 }
688
689 /**
690 * When the digital wallet button is pressed, add the product to cart and generate a new payment request.
691 * We need to add the product the cart to help with shipping/tax calculations.
692 *
693 * @since 2.3
694 * @return void
695 */
696 public function ajax_add_to_cart() {
697 check_ajax_referer( 'wc-square-add-to-cart', 'security' );
698
699 try {
700 $product_id = ! empty( $_POST['product_id'] ) ? wc_clean( wp_unslash( $_POST['product_id'] ) ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
701 $quantity = ! empty( $_POST['quantity'] ) ? wc_clean( wp_unslash( $_POST['quantity'] ) ) : 1; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
702 $attributes = ! empty( $_POST['attributes'] ) ? wc_clean( wp_unslash( $_POST['attributes'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
703
704 try {
705 $payment_request = $this->get_product_payment_request( $product_id, $quantity, $attributes, true );
706 } catch ( \Exception $e ) {
707 wp_send_json_error( $e->getMessage() );
708 }
709
710 $response = array(
711 'payment_request' => $payment_request,
712 // We need to generate a new set of nonces now that a WC customer session exists after a product was added to the cart
713 'payment_request_nonce' => wp_create_nonce( 'wc-square-get-payment-request' ),
714 'add_to_cart_nonce' => wp_create_nonce( 'wc-square-add-to-cart' ),
715 'recalculate_totals_nonce' => wp_create_nonce( 'wc-square-recalculate-totals' ),
716 'process_checkout_nonce' => wp_create_nonce( 'woocommerce-process_checkout' ),
717 );
718
719 wp_send_json_success( wp_json_encode( $response ) );
720 } catch ( \Exception $e ) {
721 wp_send_json_error( $e->getMessage() );
722 }
723 }
724
725 /**
726 * Updates shipping method in WC session
727 *
728 * @since 2.3
729 * @param array $shipping_methods Array of selected shipping methods ids
730 * @return void
731 */
732 public function update_shipping_method( $shipping_methods ) {
733 $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
734
735 if ( is_array( $shipping_methods ) ) {
736 foreach ( $shipping_methods as $i => $value ) {
737 $chosen_shipping_methods[ $i ] = wc_clean( $value );
738 }
739 }
740
741 WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
742 }
743
744 /**
745 * Reset shipping and calculate the latest shipping options/package with the given address.
746 *
747 * If no address, use the store's base address as default.
748 *
749 * @since 2.3
750 * @param array $address
751 * @return void
752 */
753 public function calculate_shipping( $address = array() ) {
754 WC()->shipping->reset_shipping();
755
756 if ( $address['countryCode'] ) {
757 WC()->customer->set_location( strtoupper( $address['countryCode'] ), $address['state'], $address['postalCode'], $address['city'] );
758 WC()->customer->set_shipping_location( strtoupper( $address['countryCode'] ), $address['state'], $address['postalCode'], $address['city'] );
759 } else {
760 WC()->customer->set_billing_address_to_base();
761 WC()->customer->set_shipping_address_to_base();
762 }
763
764 WC()->customer->set_calculated_shipping( true );
765 WC()->customer->save();
766
767 $packages = array();
768 $packages[0]['contents'] = WC()->cart->get_cart();
769 $packages[0]['contents_cost'] = 0;
770 $packages[0]['applied_coupons'] = WC()->cart->applied_coupons;
771 $packages[0]['user']['ID'] = get_current_user_id();
772 $packages[0]['destination']['country'] = $address['countryCode'];
773 $packages[0]['destination']['state'] = $address['state'];
774 $packages[0]['destination']['postcode'] = $address['postalCode'];
775 $packages[0]['destination']['city'] = $address['city'];
776 $packages[0]['destination']['address'] = $address['address'];
777 $packages[0]['destination']['address_2'] = $address['address_2'];
778
779 foreach ( WC()->cart->get_cart() as $item ) {
780 if ( $item['data']->needs_shipping() ) {
781 if ( isset( $item['line_total'] ) ) {
782 $packages[0]['contents_cost'] += $item['line_total'];
783 }
784 }
785 }
786
787 /**
788 * Hook to filter shipping packages.
789 *
790 * @param array Array of shipping packages.
791 * @since 2.3
792 */
793 $packages = apply_filters( 'woocommerce_cart_shipping_packages', $packages );
794
795 WC()->shipping->calculate_shipping( $packages );
796 }
797
798 /**
799 * Returns location's state code by state name.
800 *
801 * @param string $country_code The country's 2 letter ISO 3166-1 alpha-2 code.
802 * @param string $state_name The full name of the state that is to be search for its code.
803 *
804 * @return string
805 */
806 public static function get_state_code_by_name( $country_code = '', $state_name = '' ) {
807 if ( empty( $country_code ) || empty( $state_name ) ) {
808 return '';
809 }
810
811 $states = WC()->countries->get_states( $country_code );
812
813 /**
814 * Check for valid country code that don't have list of states,
815 * return state code as it is.
816 */
817 $countries = WC()->countries->get_countries();
818
819 if ( false === $states && isset( $countries[ $country_code ] ) ) {
820 return $state_name;
821 }
822
823 if ( is_array( $states ) ) {
824 /** Return the state code if $state_name already contains a valid state code. */
825 if ( isset( $states[ $state_name ] ) ) {
826 return $state_name;
827 }
828
829 foreach ( $states as $code => $name ) {
830 if ( $name === $state_name ) {
831 return $code;
832 }
833 }
834 }
835
836 return '';
837 }
838
839 /**
840 * Recalculate shipping methods and cart totals and send the updated information
841 * data as a square payment request json object.
842 *
843 * @since 2.3
844 * @return void
845 */
846 public function ajax_recalculate_totals() {
847 check_ajax_referer( 'wc-square-recalculate-totals', 'security' );
848
849 $chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
850 $shipping_address = array();
851 $payment_request = array();
852
853 $is_pay_for_order_page = isset( $_POST['is_pay_for_order_page'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['is_pay_for_order_page'] ) ) : is_wc_endpoint_url( 'order-pay' );
854 $order_id = isset( $_POST['order_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : absint( get_query_var( 'order-pay' ) );
855 $order_data = array();
856
857 if ( $is_pay_for_order_page ) {
858 $auth_order = wc_get_order( $order_id );
859 // Pay-for-order AJAX calls to recalculate totals may be abused to leak order totals.
860 // Gate before shipping/total logic runs so this cannot leak unauthenticated totals.
861 if ( ! Order_Ajax_Authorization::is_authorized_for_pay_for_order( $auth_order ) ) {
862 wp_send_json_error( Order_Ajax_Authorization::get_invalid_order_message() );
863 }
864 }
865
866 if ( WC()->cart->needs_shipping() || $is_pay_for_order_page ) {
867 if ( ! empty( $_POST['shipping_contact'] ) ) {
868 $shipping_address = wp_parse_args(
869 wc_clean( wp_unslash( $_POST['shipping_contact'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
870 array(
871 'countryCode' => null,
872 'state' => null,
873 'city' => null,
874 'postalCode' => null,
875 'address' => null,
876 'address_2' => null,
877 )
878 );
879
880 /**
881 * WooCommerce requires state code but for few countries, Google Pay
882 * returns the state's full name instead of the state code.
883 *
884 * The following line converts state name to code.
885 */
886 if ( isset( $shipping_address['countryCode'] ) && isset( $shipping_address['state'] ) ) {
887 $shipping_address['state'] = self::get_state_code_by_name( $shipping_address['countryCode'], $shipping_address['state'] );
888 }
889
890 $this->calculate_shipping( $shipping_address );
891
892 $packages = WC()->shipping->get_packages();
893 $packages = array_values( $packages ); /// reindex the array.
894
895 if ( ! empty( $packages ) ) {
896 foreach ( $packages[0]['rates'] as $method ) {
897 $payment_request['shippingOptions'][] = array(
898 'id' => $method->id,
899 'label' => $method->get_label(),
900 'amount' => number_format( $method->cost, 2, '.', '' ),
901 );
902 }
903 }
904
905 // sort the shippingOptions so that the default/chosen shipping method is the first option so that it's displayed first in the Apple Pay/Google Pay window
906 if ( isset( $payment_request['shippingOptions'][0] ) ) {
907 if ( isset( $chosen_methods[0] ) ) {
908 $chosen_method_id = $chosen_methods[0];
909 $compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) {
910 if ( $a['id'] === $chosen_method_id ) {
911 return -1;
912 }
913
914 if ( $b['id'] === $chosen_method_id ) {
915 return 1;
916 }
917
918 return 0;
919 };
920
921 usort( $payment_request['shippingOptions'], $compare_shipping_options );
922 }
923
924 $first_shipping_method_id = $payment_request['shippingOptions'][0]['id'];
925 $this->update_shipping_method( array( $first_shipping_method_id ) );
926 }
927 } elseif ( ! empty( $_POST['shipping_option'] ) ) {
928 $chosen_methods = array( wc_clean( wp_unslash( $_POST['shipping_option'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
929 $this->update_shipping_method( $chosen_methods );
930 }
931 }
932
933 if ( ! $is_pay_for_order_page ) {
934 WC()->cart->calculate_totals();
935 }
936
937 if ( $is_pay_for_order_page ) {
938 $order = wc_get_order( $order_id );
939 $order_data = array(
940 'subtotal' => $order->get_subtotal(),
941 'discount' => $order->get_discount_total(),
942 'shipping' => $order->get_shipping_total(),
943 'fees' => $order->get_total_fees(),
944 'taxes' => $order->get_total_tax(),
945 );
946 }
947
948 $payment_request['lineItems'] = $this->build_payment_request_line_items( $order_data );
949
950 if ( $is_pay_for_order_page ) {
951 $total_amount = $order->get_total();
952 } else {
953 $total_amount = WC()->cart->total;
954 }
955
956 $payment_request['total'] = array(
957 'label' => get_bloginfo( 'name', 'display' ) . esc_html( $this->total_label_suffix ),
958 'amount' => number_format( $total_amount, 2, '.', '' ),
959 'pending' => false,
960 );
961
962 wp_send_json_success( $payment_request );
963 }
964
965 /**
966 * Filters the post data just before checkout.
967 *
968 * WooCommerce requires the state code but Google Pay returns
969 * the full name of the state. We filter the post data to convert
970 * the full state name into its equivalent state code.
971 *
972 * @param array $posted_data The $_POST data submitted at checkout.
973 *
974 * @return array
975 */
976 public function filter_posted_data( $posted_data ) {
977 if ( isset( $posted_data['payment_method'] ) && 'square_credit_card' !== $posted_data['payment_method'] ) {
978 return $posted_data;
979 }
980
981 if ( isset( $posted_data['shipping_country'] ) && isset( $posted_data['shipping_state'] ) ) {
982 $posted_data['shipping_state'] = self::get_state_code_by_name( $posted_data['shipping_country'], $posted_data['shipping_state'] );
983 }
984
985 if ( isset( $posted_data['billing_country'] ) && isset( $posted_data['billing_state'] ) ) {
986 $posted_data['billing_state'] = self::get_state_code_by_name( $posted_data['billing_country'], $posted_data['billing_state'] );
987 }
988
989 return $posted_data;
990 }
991
992 /**
993 * Filters checkout fields when Digital Wallet is used.
994 *
995 * @param array $fields Array of checkout fields.
996 *
997 * @return array
998 */
999 public function filter_checkout_fields( $fields ) {
1000 /** Ignoring nonce verification as that is already taken care of in WC_Checkout::process_checkout. */
1001 $wallet_type = isset( $_POST['wc-square-digital-wallet-type'] ) ? wc_clean( wp_unslash( $_POST['wc-square-digital-wallet-type'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1002
1003 if ( ! $wallet_type ) {
1004 return $fields;
1005 }
1006
1007 if ( isset( $fields['billing'] ) && isset( $fields['billing']['billing_company'] ) ) {
1008 $fields['billing']['billing_company']['required'] = false;
1009 }
1010
1011 if ( isset( $fields['shipping'] ) && isset( $fields['shipping']['shipping_company'] ) ) {
1012 $fields['shipping']['shipping_company']['required'] = false;
1013 }
1014
1015 return $fields;
1016 }
1017
1018 /**
1019 * Process the digital wallet checkout
1020 *
1021 * @since 2.3
1022 * @return void
1023 */
1024 public function ajax_process_checkout() {
1025 if ( WC()->cart->is_empty() ) {
1026 wp_send_json_error( __( 'Empty cart', 'woocommerce-square' ) );
1027 }
1028
1029 if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) {
1030 define( 'WOOCOMMERCE_CHECKOUT', true );
1031 }
1032
1033 WC()->checkout()->process_checkout();
1034
1035 die( 0 );
1036 }
1037
1038
1039 /** Helper methods *******************************************************************************************/
1040
1041
1042 /**
1043 * Helper function to return the expected location of the apple-developer verification file on the server.
1044 *
1045 * @since 2.3
1046 * @return string
1047 */
1048 public function apple_pay_verification_file_location() {
1049 return ! empty( $_SERVER['DOCUMENT_ROOT'] ) ? untrailingslashit( wc_clean( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) ) . '/.well-known/apple-developer-merchantid-domain-association' : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1050 }
1051
1052 /**
1053 * Checks for the existance of Apple Pay verification domain file at:
1054 * SERVER_ROOT/.well-known/apple-developer-merchantid-domain-association
1055 *
1056 * If the file doesn't exist or the contents has been modified, copy the file from the plugin
1057 * directory to the expected verification domain file location.
1058 *
1059 * @since 2.3
1060 * @return bool
1061 */
1062 public function check_apple_pay_verification_file() {
1063 if ( empty( $_SERVER['DOCUMENT_ROOT'] ) ) {
1064 return false;
1065 }
1066
1067 $path = untrailingslashit( wc_clean( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1068 $dir = '.well-known';
1069 $file = 'apple-developer-merchantid-domain-association';
1070 $fullpath = $path . '/' . $dir . '/' . $file;
1071 $plugin_path = $this->gateway->get_plugin()->get_plugin_path();
1072 $existing_contents = @file_get_contents( $fullpath ); // @codingStandardsIgnoreLine
1073 $new_contents = @file_get_contents( $plugin_path . '/' . $file ); // @codingStandardsIgnoreLine
1074
1075 if ( $existing_contents && $existing_contents === $new_contents ) {
1076 return true;
1077 }
1078
1079 if ( ! file_exists( $path . '/' . $dir ) ) {
1080 if ( ! @mkdir( $path . '/' . $dir, 0755 ) ) { // @codingStandardsIgnoreLine
1081 $this->gateway->get_plugin()->log( 'Unable to create domain association folder to domain root.' );
1082 return false;
1083 }
1084 }
1085
1086 if ( ! @copy( $plugin_path . '/' . $file, $fullpath ) ) { // @codingStandardsIgnoreLine
1087 $this->gateway->get_plugin()->log( 'Unable to copy domain association file to domain root.' );
1088 return false;
1089 }
1090
1091 $this->gateway->get_plugin()->log( 'Apple Pay Domain association file updated.' );
1092 return true;
1093 }
1094
1095 /**
1096 * When loading the settings page this function, tries to register the current store domain with Square/Apple Pay.
1097 *
1098 * If digital wallets and Apple Pay is enabled, check that the domain verification file exist and check
1099 * the gateway settings for `apple_pay_domain_registered` to confirm that this domain has been successfully registered with Square/Apple Pay.
1100 *
1101 * If the store has been registered, keep verifying the registration of the current connected account and domain every hour.
1102 *
1103 * @since 2.3
1104 */
1105 public function apple_pay_domain_registration() {
1106 // Only register the store url with Apple Pay if the gateway and digital wallets are enable (check POST data to account for the page load when settings are being saved).
1107 if ( ( 'no' === $this->gateway->get_option( 'enabled', 'no' ) && empty( $_POST['woocommerce_square_credit_card_enabled'] ) ) || ( 'no' === $this->gateway->get_option( 'enable_digital_wallets', 'yes' ) && empty( $_POST['woocommerce_square_credit_card_enable_digital_wallets'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1108 return;
1109 }
1110
1111 // when settings are being saved, make sure we use the latest values from POST data to check if Apple isn't one of the hidden wallet options
1112 $hidden_wallet_options = ! isset( $_POST['woocommerce_square_credit_card_enable_digital_wallets'] ) ? $this->gateway->get_option( 'digital_wallets_hide_button_options', array() ) : ( ! empty( $_POST['woocommerce_square_credit_card_digital_wallets_hide_button_options'] ) ? wc_clean( wp_unslash( $_POST['woocommerce_square_credit_card_digital_wallets_hide_button_options'] ) ) : array() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1113 if ( in_array( 'apple', $hidden_wallet_options, true ) ) {
1114 return;
1115 }
1116
1117 if ( ! $this->check_apple_pay_verification_file() ) {
1118 $this->gateway->update_option( 'apple_pay_domain_registered', 'no' );
1119 return;
1120 }
1121
1122 try {
1123 $recently_registered = get_transient( 'wc_square_check_apple_pay_domain_registration' );
1124
1125 if ( 'no' === $this->gateway->get_option( 'apple_pay_domain_registered', 'no' ) || ! $recently_registered ) {
1126 $this->register_apple_pay_domain();
1127
1128 $this->gateway->update_option( 'apple_pay_domain_registered', 'yes' );
1129 $this->gateway->get_plugin()->log( 'Your domain has been verified with Apple Pay!' );
1130
1131 // avoid API rate limits by limiting the request to checking every hour
1132 set_transient( 'wc_square_check_apple_pay_domain_registration', true, HOUR_IN_SECONDS );
1133 }
1134 } catch ( \Exception $e ) {
1135 $this->gateway->update_option( 'apple_pay_domain_registered', 'no' );
1136 $this->gateway->get_plugin()->log( 'Error: ' . $e->getMessage() );
1137 }
1138 }
1139
1140 /**
1141 * Sends an API request to endpoint v2/apple-pay/domains to register the store's domain with Square/Apple Pay.
1142 *
1143 * Reference: https://developer.squareup.com/docs/payment-form/cookbook/apple-pay-register-domains
1144 *
1145 * @since 2.3
1146 * @throws \Exception on error
1147 * @return void
1148 */
1149 private function register_apple_pay_domain() {
1150 $access_token = $this->gateway->get_plugin()->get_settings_handler()->get_access_token();
1151 $is_sandbox = $this->gateway->get_plugin()->get_settings_handler()->is_sandbox();
1152 $domain_name = ! empty( $_SERVER['HTTP_HOST'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1153
1154 $this->gateway->update_option( 'apple_pay_domain_registration_attempted', 'yes' );
1155
1156 if ( empty( $domain_name ) ) {
1157 throw new \Exception( 'Unable to verify domain with Apple Pay - no domain found in $_SERVER[\'HTTP_HOST\'].' );
1158 }
1159
1160 if ( empty( $access_token ) ) {
1161 throw new \Exception( esc_html__( 'Unable to verify domain with Apple Pay - missing access token.', 'woocommerce-square' ) );
1162 }
1163
1164 $response = wp_remote_post(
1165 'https://connect.squareup' . ( $is_sandbox ? 'sandbox' : '' ) . '.com/v2/apple-pay/domains',
1166 array(
1167 'headers' => array(
1168 'Square-Version' => '2020-10-28',
1169 'Authorization' => 'Bearer ' . $access_token,
1170 'Content-Type' => 'application/json',
1171 ),
1172 'body' => wp_json_encode(
1173 array(
1174 'domain_name' => $domain_name,
1175 )
1176 ),
1177 )
1178 );
1179
1180 if ( is_wp_error( $response ) ) {
1181 /* translators: error message */
1182 throw new \Exception( sprintf( 'Unable to verify domain %s - %s', esc_html( $domain_name ), esc_html( $response->get_error_message() ) ) );
1183 }
1184
1185 $parsed_response = json_decode( $response['body'], true );
1186
1187 if ( 200 !== $response['response']['code'] || empty( $parsed_response['status'] ) || 'VERIFIED' !== $parsed_response['status'] ) {
1188 /* translators: error message */
1189 throw new \Exception( sprintf( 'Unable to verify domain %s - response = %s', esc_html( $domain_name ), esc_html( print_r( $parsed_response, true ) ) ) ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1190 }
1191 }
1192
1193 /**
1194 * Returns an array of pages that digital wallets are loaded/available on.
1195 * Defaults to product, cart and checkout pages.
1196 *
1197 * @since 2.3
1198 * @return array
1199 */
1200 public function get_available_pages() {
1201 /**
1202 * Hook to filter pages which should load digital wallets.
1203 *
1204 * @param array Array of page names.
1205 * @since 2.3
1206 */
1207 return apply_filters(
1208 'wc_square_display_digital_wallet_on_pages',
1209 array(
1210 'product',
1211 'cart',
1212 'checkout',
1213 ),
1214 $this
1215 );
1216 }
1217
1218 /**
1219 * Returns the current page.
1220 *
1221 * Stores the result in $this->page to avoid recalculating multiple times per request
1222 *
1223 * @since 2.3
1224 * @return string
1225 */
1226 public function get_current_page() {
1227 if ( null === $this->page ) {
1228 $is_cart = is_cart() && ! WC()->cart->is_empty();
1229 $is_product = is_product() || wc_post_content_has_shortcode( 'product_page' );
1230 $this->page = null;
1231
1232 if ( $is_cart ) {
1233 $this->page = 'cart';
1234 } elseif ( $is_product ) {
1235 $this->page = 'product';
1236 } elseif ( is_checkout() || ( function_exists( 'has_block' ) && has_block( 'woocommerce/checkout' ) ) ) {
1237 $this->page = 'checkout';
1238 }
1239 }
1240
1241 return $this->page;
1242 }
1243
1244 /**
1245 * Returns cart totals in an array format
1246 *
1247 * @since 2.3
1248 * @throws \Exception if no cart is found
1249 * @return array
1250 */
1251 public function get_cart_totals() {
1252 if ( ! isset( WC()->cart ) ) {
1253 throw new \Exception( 'Cart data cannot be found.' );
1254 }
1255
1256 return array(
1257 'subtotal' => WC()->cart->subtotal_ex_tax,
1258 'discount' => WC()->cart->get_cart_discount_total(),
1259 'shipping' => WC()->cart->shipping_total,
1260 'fees' => WC()->cart->fee_total,
1261 'taxes' => WC()->cart->tax_total + WC()->cart->shipping_tax_total,
1262 );
1263 }
1264
1265 /**
1266 * Returns a list of hidden digital wallet options
1267 *
1268 * If Apple Pay domain hasn't been registered, force Apple Pay to be hidden.
1269 *
1270 * @since 2.3
1271 * @return array
1272 */
1273 public function get_hidden_button_options() {
1274 $hidden_options = $this->gateway->get_option( 'digital_wallets_hide_button_options', array() );
1275
1276 if ( ( ! is_array( $hidden_options ) || ! in_array( 'apple', $hidden_options, true ) ) && 'no' === $this->gateway->get_option( 'apple_pay_domain_registered', 'no' ) ) {
1277 $hidden_options[] = 'apple';
1278 }
1279
1280 return $hidden_options;
1281 }
1282
1283 /**
1284 * Returns a list the supported product types that can be used to purchase a digital wallet
1285 *
1286 * @since 2.3
1287 * @return array
1288 */
1289 public function supported_product_types() {
1290 /**
1291 * Hook to filter array of post types that can support digital wallets.
1292 *
1293 * @param array Array of supported post types.
1294 * @since 2.3
1295 */
1296 return apply_filters(
1297 'wc_square_digital_wallets_supported_product_types',
1298 array(
1299 'simple',
1300 'variable',
1301 'variation',
1302 'booking',
1303 'bundle',
1304 'composite',
1305 'mix-and-match',
1306 )
1307 );
1308 }
1309
1310 /**
1311 * Checks if digital wallets are allowed to be used to purchase the current product.
1312 *
1313 * @since 2.3
1314 * @return bool
1315 */
1316 public function allowed_for_product_page() {
1317 global $post;
1318
1319 $product = wc_get_product( $post->ID );
1320
1321 if ( ! is_object( $product ) || ! in_array( $product->get_type(), $this->supported_product_types(), true ) ) {
1322 return false;
1323 }
1324
1325 // Trial subscriptions with shipping are not supported
1326 if ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && \WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) {
1327 return false;
1328 }
1329
1330 // Pre Orders charge upon release not supported.
1331 if ( class_exists( 'WC_Pre_Orders_Product' ) && \WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) {
1332 return false;
1333 }
1334
1335 // File upload addon not supported
1336 if ( class_exists( 'WC_Product_Addons_Helper' ) ) {
1337 $product_addons = \WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
1338 foreach ( $product_addons as $addon ) {
1339 if ( 'file_upload' === $addon['type'] ) {
1340 return false;
1341 }
1342 }
1343 }
1344
1345 return true;
1346 }
1347
1348 /**
1349 * Checks the cart to see if Square Digital Wallets is allowed to purchase all cart items.
1350 *
1351 * @since 2.3
1352 * @return bool
1353 */
1354 public function allowed_for_cart() {
1355 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1356 /**
1357 * Hook to filter cart item product.
1358 *
1359 * @param array $cart_item['data] Product object.
1360 * @param array $cart_item Cart item.
1361 * @param string $cart_item_key Cart item key.
1362 * @since 2.3
1363 */
1364 $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
1365
1366 if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) {
1367 return false;
1368 }
1369
1370 // Trial subscriptions with shipping are not supported
1371 if ( class_exists( 'WC_Subscriptions_Cart' ) && class_exists( 'WC_Subscriptions_Product' ) && \WC_Subscriptions_Cart::cart_contains_subscription() && $_product->needs_shipping() && \WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) {
1372 return false;
1373 }
1374
1375 // Pre Orders compatibility where we don't support charge upon release.
1376 if ( class_exists( 'WC_Pre_Orders_Cart' ) && class_exists( 'WC_Pre_Orders_Product' ) && \WC_Pre_Orders_Cart::cart_contains_pre_order() && \WC_Pre_Orders_Product::product_is_charged_upon_release( \WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
1377 return false;
1378 }
1379 }
1380
1381 return true;
1382 }
1383
1384 /**
1385 * Returns if Google Pay and/or Apple Pay is available by checking the following:
1386 * - setting is enabled
1387 * - country and currency is supported
1388 * - square is connected and location is set
1389 * - current page/cart has items that can be purchased with a digital wallet
1390 *
1391 * Sets $this->is_available so that it's only checked once per request/page load
1392 *
1393 * @since 2.3
1394 * @return bool
1395 */
1396 public function is_available() {
1397 if ( null === $this->is_available ) {
1398 $this->is_available = $this->is_available_for_page() && $this->is_digital_wallet_enabled() && $this->gateway->is_available();
1399 }
1400
1401 return $this->is_available;
1402 }
1403
1404 /**
1405 * Returns true if digital wallets are available for the given page.
1406 * Checks the current page is no $page value is given
1407 *
1408 * @since 2.4.0
1409 * @param string $page
1410 * @return bool
1411 */
1412 public function is_available_for_page( $page = '' ) {
1413 $is_available = true;
1414 $page = ! empty( $page ) ? $page : $this->get_current_page();
1415 $available_pages = $this->get_available_pages();
1416
1417 if ( empty( $page ) || ! is_array( $available_pages ) || ! in_array( $page, $available_pages, true ) ) {
1418 $is_available = false;
1419 }
1420
1421 if ( $is_available && 'product' === $page ) {
1422 $is_available = $this->allowed_for_product_page();
1423 } elseif ( $is_available && ( 'cart' === $page || 'checkout' === $page ) ) {
1424 $is_available = $this->allowed_for_cart();
1425 }
1426
1427 return $is_available;
1428 }
1429
1430 /**
1431 * Checks if digital wallets are available and enabled.
1432 *
1433 * @since 2.4
1434 * @return bool
1435 */
1436 public function is_digital_wallet_enabled() {
1437 return $this->gateway->is_digital_wallet_available() && 'yes' === $this->gateway->get_option( 'enable_digital_wallets', 'yes' );
1438 }
1439
1440 /**
1441 * This function calculates the value returned by get_option( 'wc_square_apple_pay_enabled', $default )
1442 * and is used by WC Admin's Remote Inbox Notifications for marketing purposes.
1443 *
1444 * Returns either 1 or 2 if Apple Pay is enabled on the store, or $value (false) if not
1445 *
1446 * @since 2.3
1447 * @param $value
1448 * @return int|mixed
1449 */
1450 public function get_option_is_apple_pay_enabled( $value ) {
1451 if ( $this->is_digital_wallet_enabled() && ! in_array( 'apple', $this->gateway->get_option( 'digital_wallets_hide_button_options', array() ), true ) ) {
1452 $value = wp_rand( 1, 2 );
1453 }
1454
1455 return $value;
1456 }
1457 }
1458