Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use Woo's cart fee for covering transaction fees #2820

Merged
merged 4 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();