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