Skip to content

Commit

Permalink
fix: use Woo's cart fee for covering transaction fees (#2820)
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelpeixe authored Dec 22, 2023
1 parent 1af3c23 commit fded027
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 83 deletions.
29 changes: 29 additions & 0 deletions assets/other-scripts/wc-cover-fees/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* globals jQuery, newspack_wc_cover_fees */
( function ( $ ) {
if ( ! $ ) {
return;
}
const $body = $( document.body );
$body.on( 'init_checkout', function () {
const form = document.querySelector( 'form.checkout' );
if ( ! form ) {
return;
}
let checked = document.getElementById( newspack_wc_cover_fees.custom_field_name )?.checked;
form.addEventListener( 'change', function () {
// Get element on every change because the DOM is replaced by AJAX.
const input = document.getElementById( newspack_wc_cover_fees.custom_field_name );
if ( ! input ) {
return;
}
if ( checked !== input.checked ) {
checked = input.checked;
$body.trigger( 'update_checkout', { update_shipping_method: false } );
}
} );
// Trigger checkout update on payment method change so it updates the fee.
$( document ).on( 'payment_method_selected', function () {
$body.trigger( 'update_checkout', { update_shipping_method: false } );
} );
} );
} )( jQuery );
171 changes: 88 additions & 83 deletions includes/reader-revenue/woocommerce/class-woocommerce-cover-fees.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@
* WooCommerce Order UTM class.
*/
class WooCommerce_Cover_Fees {
const CUSTOM_FIELD_NAME = 'newspack-wc-pay-fees';
const PRICE_ELEMENT_ID = 'newspack-wc-price';
const WC_ORDER_META_NAME = 'newspack_donor_covers_fees';
const CUSTOM_FIELD_NAME = 'newspack-wc-pay-fees';

/**
* Initialize hooks.
*/
public static function init() {
\add_filter( 'woocommerce_checkout_fields', [ __CLASS__, 'add_checkout_fields' ] );
\add_filter( 'woocommerce_checkout_create_order', [ __CLASS__, 'set_total_with_fees' ], 1, 2 );
\add_action( 'woocommerce_checkout_update_order_review', [ __CLASS__, 'persist_fee_selection' ] );
\add_action( 'woocommerce_cart_calculate_fees', [ __CLASS__, 'add_transaction_fee' ] );
\add_action( 'woocommerce_checkout_order_processed', [ __CLASS__, 'add_order_note' ], 1, 3 );
\add_filter( 'wc_price', [ __CLASS__, 'amend_price_markup' ], 1, 2 );
\add_action( 'wc_stripe_payment_fields_stripe', [ __CLASS__, 'render_stripe_input' ] );
\add_action( 'wp_enqueue_scripts', [ __CLASS__, 'print_checkout_helper_script' ] );
}
Expand All @@ -50,19 +48,41 @@ public static function add_checkout_fields( $fields ) {
}

/**
* Set order total, taking the fee into account.
* Persist the transaction fee selection in the Woo sesion when updating the
* order review.
*
* @param \WC_Order $order Order object.
* @param array $data Posted data.
* @param string $posted_data Posted posted_data.
*/
public static function persist_fee_selection( $posted_data ) {
$data = [];
parse_str( $posted_data, $data );
if ( self::should_apply_fee( $data ) ) {
\WC()->session->set( self::CUSTOM_FIELD_NAME, 1 );
} else {
\WC()->session->set( self::CUSTOM_FIELD_NAME, 0 );
}
}

/**
* Add fee.
*
* @return \WC_Order
* @param \WC_Cart $cart Cart object.
*/
public static function set_total_with_fees( $order, $data ) {
if ( isset( $data[ self::CUSTOM_FIELD_NAME ] ) && 1 === $data[ self::CUSTOM_FIELD_NAME ] ) {
$order->add_meta_data( self::WC_ORDER_META_NAME, 1 );
$order->set_total( self::get_total_with_fee( $order->get_total() ) );
public static function add_transaction_fee( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
if ( ! \WC()->session->get( self::CUSTOM_FIELD_NAME ) ) {
return;
}
return $order;
$cart->add_fee(
sprintf(
// Translators: %s is the fee percentage.
__( 'Transaction fee (%s)', 'newspack-plugin' ),
self::get_fee_display_value()
),
self::get_fee_value()
);
}

/**
Expand All @@ -73,8 +93,8 @@ public static function set_total_with_fees( $order, $data ) {
* @param \WC_Order $order Order object.
*/
public static function add_order_note( $order_id, $posted_data, $order ) {
if ( 1 === intval( $order->get_meta_data( self::WC_ORDER_META_NAME ) ) ) {
$order->add_order_note( __( 'The donor opted to cover Stripe\'s transaction fee. The total amount will be updated.', 'newspack-plugin' ) );
if ( \WC()->session->get( self::CUSTOM_FIELD_NAME ) ) {
$order->add_order_note( __( 'The donor opted to cover Stripe\'s transaction fee.', 'newspack-plugin' ) );
}
}

Expand All @@ -100,16 +120,27 @@ private static function should_allow_covering_fees() {
if ( true !== boolval( get_option( 'newspack_donations_allow_covering_fees' ) ) ) {
return false;
}
if ( \Newspack_Blocks\Modal_Checkout::is_modal_checkout() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return true;
return true;
}

/**
* Whether to apply the fee in the current request.
*
* @param array $data Posted data.
*
* @return bool
*/
private static function should_apply_fee( $data ) {
if ( ! self::should_allow_covering_fees() ) {
return false;
}
if ( isset( $_POST['post_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
parse_str( \sanitize_text_field( \wp_unslash( $_POST['post_data'] ) ), $post_data ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( isset( $post_data['modal_checkout'] ) && 1 === intval( $post_data['modal_checkout'] ) ) {
return true;
}
if ( ! isset( $data['payment_method'] ) || 'stripe' !== $data['payment_method'] ) {
return false;
}
if ( ! isset( $data[ self::CUSTOM_FIELD_NAME ] ) || '1' !== $data[ self::CUSTOM_FIELD_NAME ] ) {
return false;
}
return false;
return true;
}

/**
Expand All @@ -123,16 +154,15 @@ public static function render_stripe_input() {
<fieldset>
<p class="form-row newspack-cover-fees" style="display: flex;">
<input
id=<?php echo esc_attr( self::CUSTOM_FIELD_NAME ); ?>
name=<?php echo esc_attr( self::CUSTOM_FIELD_NAME ); ?>
id="<?php echo esc_attr( self::CUSTOM_FIELD_NAME ); ?>"
name="<?php echo esc_attr( self::CUSTOM_FIELD_NAME ); ?>"
type="checkbox"
value="true"
style="margin-right: 8px;"
onchange="newspackHandleCoverFees(this)"
value="1"
<?php if ( get_option( 'newspack_donations_allow_covering_fees_default', false ) ) : ?>
checked
<?php endif; ?>
>
/>
<label for=<?php echo esc_attr( self::CUSTOM_FIELD_NAME ); ?> style="display:inline;">
<?php
$custom_message = get_option( 'newspack_donations_allow_covering_fees_label', '' );
Expand Down Expand Up @@ -166,58 +196,27 @@ private static function get_possessive( $string ) {
return $string . '' . ( 's' !== $string[ strlen( $string ) - 1 ] ? 's' : '' );
}

/**
* Amend the price markup so it's possible to manipulate it via JS.
*
* @param string $html Price HTML.
* @param string $price Price.
*/
public static function amend_price_markup( $html, $price ) {
if ( ! self::should_allow_covering_fees() ) {
return $html;
}
return str_replace( $price, '<span id="' . self::PRICE_ELEMENT_ID . '">' . $price . '</span>', $html );
}

/**
* Print the checkout helper JS script.
*/
public static function print_checkout_helper_script() {
if ( ! self::should_allow_covering_fees() ) {
return;
}
$handler = 'newspack-wc-modal-checkout-helper';
wp_register_script( $handler, '', [], false, [ 'in_footer' => true ] ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion
wp_enqueue_script( $handler );
$original_price = WC()->cart->total;
$price_with_fee = number_format( self::get_total_with_fee( $original_price ), 2 );
$has_coupons_available = WC()->cart->get_coupon_discount_totals();
$coupons_handling_script = '';
if ( \wc_coupons_enabled() ) {
// Handle an edge case where the price was updated in the UI, and then a coupon was applied.
// In this case, the price has to be reverted to the original value, since covering fees
// is not supported with coupons.
$coupons_handling_script = 'setInterval(function(){
if(document.querySelector(".woocommerce-remove-coupon")){
document.getElementById( "' . self::PRICE_ELEMENT_ID . '" ).textContent = "' . $original_price . '";
}
}, 1000);';
}
wp_add_inline_script(
$handler = 'newspack-wc-cover-fees';
wp_enqueue_script(
$handler,
'const form = document.querySelector(\'form[name="checkout"]\');
if ( form ) {
form.addEventListener(\'change\', function( e ){
const inputEl = document.getElementById( "' . self::CUSTOM_FIELD_NAME . '" );
if( e.target.name === "payment_method" && e.target.value !== "stripe" && inputEl.checked ){
inputEl.checked = false;
newspackHandleCoverFees(inputEl);
}
});
}
function newspackHandleCoverFees(inputEl) {
document.getElementById( "' . self::PRICE_ELEMENT_ID . '" ).textContent = inputEl.checked ? "' . $price_with_fee . '" : "' . $original_price . '";
};' . $coupons_handling_script
\Newspack\Newspack::plugin_url() . '/dist/other-scripts/wc-cover-fees.js',
[ 'jquery' ],
NEWSPACK_PLUGIN_VERSION,
[ 'in_footer' => true ]
);
wp_localize_script(
$handler,
'newspack_wc_cover_fees',
[
'custom_field_name' => self::CUSTOM_FIELD_NAME,
]
);
}

Expand All @@ -239,25 +238,31 @@ private static function get_stripe_fee_static_value() {
* Get the fee display value.
*/
public static function get_fee_display_value() {
$price = floatval( WC()->cart->total );
$total = self::get_total_with_fee( $price );
$subtotal = WC()->cart->get_subtotal();
$total = self::get_total_with_fee();
// Just one decimal place, please.
$flat_percentage = (float) number_format( ( ( $total - $price ) * 100 ) / $price, 1 );
$flat_percentage = (float) number_format( ( ( $total - $subtotal ) * 100 ) / $subtotal, 1 );
return $flat_percentage . '%';
}

/**
* Get the fee value.
*/
public static function get_fee_value() {
$fee_multiplier = self::get_stripe_fee_multiplier_value();
$fee_static = self::get_stripe_fee_static_value();
$subtotal = WC()->cart->get_subtotal();
$fee = ( ( ( $subtotal + $fee_static ) / ( 100 - $fee_multiplier ) ) * 100 - $subtotal );
return $fee;
}

/**
* Calculate the adjusted total, taking the fee into account.
*
* @param float $total Total amount.
*
* @return float
*/
private static function get_total_with_fee( $total ) {
$fee_multiplier = self::get_stripe_fee_multiplier_value();
$fee_static = self::get_stripe_fee_static_value();
$fee = ( ( ( $total + $fee_static ) / ( 100 - $fee_multiplier ) ) * 100 - $total );
return $total + $fee;
private static function get_total_with_fee() {
return WC()->cart->get_subtotal() + self::get_fee_value();
}
}
WooCommerce_Cover_Fees::init();

0 comments on commit fded027

Please sign in to comment.