' . esc_html__( 'Total Subscribers', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->total_customers ) . wcs_help_tip( __( 'The number of unique customers with a subscription of any status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . ' ';
+ echo ' ' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->active_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) . ' ';
+ echo ' ' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->total_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . ' ';
+ echo ' ' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . wcs_help_tip( __( 'The total number of sign-up, switch and renewal orders placed with your store with a paid status (i.e. processing or complete).', 'woocommerce-subscriptions' ) ) . ' ';
+ echo ' ' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . ': ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . wcs_help_tip( __( 'The average value of all customers\' sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) . '
';
echo '
';
$this->display();
echo '';
@@ -75,7 +75,7 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
return sprintf( '%d', admin_url( 'edit.php?post_type=shop_subscription&_customer_user=' ), $user->customer_id, $user->total_subscriptions );
case 'total_subscription_order_count' :
- return sprintf( '%d', admin_url( 'edit.php?post_type=shop_order&_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count );
+ return sprintf( '%d', admin_url( 'edit.php?post_type=shop_order&_paid_subscription_orders_for_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count );
case 'customer_lifetime_value' :
return wc_price( $user->initial_order_total + $user->renewal_switch_total );
diff --git a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php
index d394bc3..d159800 100755
--- a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php
+++ b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php
@@ -46,7 +46,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
$query_end_date = date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) );
$offset = get_option( 'gmt_offset' );
- //Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
+ // Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
$this->report_data = new stdClass;
@@ -371,9 +371,9 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
$this->report_data->ended_counts = $cached_results[ $query_hash ];
// Total up the query data
- $this->report_data->signup_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) ) );
- $this->report_data->renewal_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) );
- $this->report_data->resubscribe_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) ) );
+ $this->report_data->signup_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) );
+ $this->report_data->renewal_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) );
+ $this->report_data->resubscribe_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) );
$this->report_data->new_subscription_total_count = absint( array_sum( wp_list_pluck( $this->report_data->new_subscriptions, 'count' ) ) );
$this->report_data->signup_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'count' ) ) );
$this->report_data->renewal_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) );
@@ -417,7 +417,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
$legend[] = array(
'title' => sprintf( __( '%s new subscriptions', 'woocommerce-subscriptions' ), '' . $this->report_data->new_subscription_total_count . '' ),
- 'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order.', 'woocommerce-subscriptions' ),
+ 'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order. This includes orders pending payment.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['new_count'],
'highlight_series' => 1,
);
diff --git a/includes/admin/reports/class-wcs-report-subscription-payment-retry.php b/includes/admin/reports/class-wcs-report-subscription-payment-retry.php
index 235b6c9..2797764 100755
--- a/includes/admin/reports/class-wcs-report-subscription-payment-retry.php
+++ b/includes/admin/reports/class-wcs-report-subscription-payment-retry.php
@@ -35,23 +35,39 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
private function query_report_data() {
global $wpdb;
+ // Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
+ $offset = get_option( 'gmt_offset' );
+ $site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
+ $retry_date_in_local_time = $wpdb->prepare( "CONVERT_TZ(retries.date_gmt, '+00:00', %s)", $site_timezone );
+
+ // We need to compute this on our own since 'group_by_query' from the parent class uses posts table column names.
+ switch ( $this->chart_groupby ) {
+ case 'day':
+ $this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time}), DAY({$retry_date_in_local_time})";
+ break;
+ case 'month':
+ $this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time})";
+ break;
+ }
+
$this->report_data = new stdClass;
- $query_start_date = get_gmt_from_date( date( 'Y-m-d', $this->start_date ) );
- $query_end_date = get_gmt_from_date( date( 'Y-m-d', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) );
+ $query_start_date = get_gmt_from_date( date( 'Y-m-d H:i:s', $this->start_date ) );
+ $query_end_date = get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) );
- // Get the sum of order totals for completed retires (i.e. retries which eventually succeeded in processing the failed payment)
+ // Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment)
$renewal_query = $wpdb->prepare(
- "SELECT COUNT(DISTINCT posts.ID) as count, posts.post_date as post_date, SUM(meta_order_total.meta_value) as renewal_totals
- FROM {$wpdb->prefix}posts AS orders
- INNER JOIN {$wpdb->prefix}posts AS posts ON ( orders.ID = posts.post_parent )
- LEFT JOIN {$wpdb->prefix}postmeta AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' )
- WHERE posts.post_type = 'payment_retry'
- AND posts.post_status = 'complete'
- AND posts.post_modified_gmt >= %s
- AND posts.post_modified_gmt < %s
- GROUP BY {$this->group_by_query}
- ORDER BY post_date ASC",
+ "
+ SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date, SUM(meta_order_total.meta_value) AS renewal_totals
+ FROM {$wpdb->posts} AS orders
+ INNER JOIN {$wpdb->prefix}wcs_payment_retries AS retries ON ( orders.ID = retries.order_id )
+ LEFT JOIN {$wpdb->postmeta} AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' )
+ WHERE retries.status = 'complete'
+ AND retries.date_gmt >= %s
+ AND retries.date_gmt < %s
+ GROUP BY {$this->group_by_query}
+ ORDER BY retry_date_gmt ASC
+ ",
$query_start_date,
$query_end_date
);
@@ -60,14 +76,15 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
// Get the counts for all retries, grouped by day or month and status
$retry_query = $wpdb->prepare(
- "SELECT COUNT(DISTINCT posts.ID) as count, posts.post_status as status, posts.post_date as post_date
- FROM {$wpdb->prefix}posts AS posts
- WHERE posts.post_type = 'payment_retry'
- AND posts.post_status IN ( 'complete','failed','pending' )
- AND posts.post_modified_gmt >= %s
- AND posts.post_modified_gmt < %s
- GROUP BY {$this->group_by_query}, posts.post_status
- ORDER BY posts.post_date_gmt ASC",
+ "
+ SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date
+ FROM {$wpdb->prefix}wcs_payment_retries AS retries
+ WHERE retries.status IN ( 'complete', 'failed', 'pending' )
+ AND retries.date_gmt >= %s
+ AND retries.date_gmt < %s
+ GROUP BY {$this->group_by_query}, status
+ ORDER BY retry_date_gmt ASC
+ ",
$query_start_date,
$query_end_date
);
@@ -80,7 +97,7 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
$this->report_data->retry_pending_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'count' ) ) );
$this->report_data->renewal_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) );
- $this->report_data->renewal_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) );
+ $this->report_data->renewal_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) );
}
/**
@@ -188,13 +205,13 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
global $wp_locale;
// Prepare data for report
- $retry_count = $this->prepare_chart_data( $this->report_data->retry_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
- $retry_success_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
- $retry_failure_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
- $retry_pending_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
+ $retry_count = $this->prepare_chart_data( $this->report_data->retry_data, 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
+ $retry_success_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
+ $retry_failure_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
+ $retry_pending_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
- $renewal_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
- $renewal_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby );
+ $renewal_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
+ $renewal_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'retry_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby );
// Encode in json format
$chart_data = array(
diff --git a/includes/api/legacy/class-wc-api-subscriptions.php b/includes/api/legacy/class-wc-api-subscriptions.php
index 423901a..c5ca9f3 100755
--- a/includes/api/legacy/class-wc-api-subscriptions.php
+++ b/includes/api/legacy/class-wc-api-subscriptions.php
@@ -121,7 +121,7 @@ class WC_API_Subscriptions extends WC_API_Orders {
public function get_subscriptions( $fields = null, $filter = array(), $status = null, $page = 1 ) {
// check user permissions
if ( ! current_user_can( 'read_private_shop_orders' ) ) {
- return new WP_Error( 'wcs_api_user_cannot_read_susbcription_count', __( 'You do not have permission to read the subscriptions count', 'woocommerce-subscriptions' ), array( 'status' => 401 ) );
+ return new WP_Error( 'wcs_api_user_cannot_read_subscription_count', __( 'You do not have permission to read the subscriptions count', 'woocommerce-subscriptions' ), array( 'status' => 401 ) );
}
$status = $this->format_statuses( $status );
diff --git a/includes/class-wc-subscription.php b/includes/class-wc-subscription.php
index 61dc28f..877968e 100755
--- a/includes/class-wc-subscription.php
+++ b/includes/class-wc-subscription.php
@@ -329,6 +329,8 @@ class WC_Subscription extends WC_Order {
$can_be_updated = true;
} elseif ( $this->has_status( 'pending' ) ) {
$can_be_updated = true;
+ } elseif ( $this->has_status( 'pending-cancel' ) && ( $this->is_manual() || ( false === $this->payment_method_supports( 'gateway_scheduled_payments' ) && $this->payment_method_supports( 'subscription_date_changes' ) && $this->payment_method_supports( 'subscription_reactivation' ) ) ) ) {
+ $can_be_updated = true;
} else {
$can_be_updated = false;
}
@@ -450,23 +452,33 @@ class WC_Subscription extends WC_Order {
case 'completed' : // core WC order status mapped internally to avoid exceptions
case 'active' :
- // Recalculate and set next payment date
- $stored_next_payment = $this->get_time( 'next_payment' );
- // Make sure the next payment date is more than 2 hours in the future by default
- if ( $stored_next_payment < ( gmdate( 'U' ) + apply_filters( 'woocommerce_subscription_activation_next_payment_date_threshold', 2 * HOUR_IN_SECONDS, $stored_next_payment, $old_status, $this ) ) ) { // also accounts for a $stored_next_payment of 0, meaning it's not set
-
- $calculated_next_payment = $this->calculate_date( 'next_payment' );
-
- if ( $calculated_next_payment > 0 ) {
- $this->update_dates( array( 'next_payment' => $calculated_next_payment ) );
- } elseif ( $stored_next_payment < gmdate( 'U' ) ) { // delete the stored date if it's in the past as we're not updating it (the calculated next payment date is 0 or none)
- $this->delete_date( 'next_payment' );
- }
+ if ( 'pending-cancel' === $old_status ) {
+ $this->update_dates( array(
+ 'cancelled' => 0,
+ 'end' => 0,
+ 'next_payment' => $this->get_date( 'end' ),
+ ) );
} else {
- // In case plugins want to run some code when the subscription was reactivated, but the next payment date was not recalculated.
- do_action( 'woocommerce_subscription_activation_next_payment_not_recalculated', $stored_next_payment, $old_status, $this );
+ // Recalculate and set next payment date
+ $stored_next_payment = $this->get_time( 'next_payment' );
+
+ // Make sure the next payment date is more than 2 hours in the future by default
+ if ( $stored_next_payment < ( gmdate( 'U' ) + apply_filters( 'woocommerce_subscription_activation_next_payment_date_threshold', 2 * HOUR_IN_SECONDS, $stored_next_payment, $old_status, $this ) ) ) { // also accounts for a $stored_next_payment of 0, meaning it's not set
+
+ $calculated_next_payment = $this->calculate_date( 'next_payment' );
+
+ if ( $calculated_next_payment > 0 ) {
+ $this->update_dates( array( 'next_payment' => $calculated_next_payment ) );
+ } elseif ( $stored_next_payment < gmdate( 'U' ) ) { // delete the stored date if it's in the past as we're not updating it (the calculated next payment date is 0 or none)
+ $this->delete_date( 'next_payment' );
+ }
+ } else {
+ // In case plugins want to run some code when the subscription was reactivated, but the next payment date was not recalculated.
+ do_action( 'woocommerce_subscription_activation_next_payment_not_recalculated', $stored_next_payment, $old_status, $this );
+ }
}
+
// Trial end date and end/expiration date don't change at all - they should be set when the subscription is first created
wcs_make_user_active( $this->get_user_id() );
break;
@@ -528,33 +540,53 @@ class WC_Subscription extends WC_Order {
* Handle the status transition.
*/
protected function status_transition() {
+ // Use local copy of status transition value.
+ $status_transition = $this->status_transition;
- if ( $this->status_transition ) {
- do_action( 'woocommerce_subscription_status_' . $this->status_transition['to'], $this );
+ // If we're not currently in the midst of a status transition, bail early.
+ if ( ! $status_transition ) {
+ return;
+ }
- if ( ! empty( $this->status_transition['from'] ) ) {
- /* translators: 1: old subscription status 2: new subscription status */
- $transition_note = sprintf( __( 'Status changed from %1$s to %2$s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $this->status_transition['from'] ), wcs_get_subscription_status_name( $this->status_transition['to'] ) );
+ try {
+ do_action( "woocommerce_subscription_status_{$status_transition['to']}", $this );
- do_action( 'woocommerce_subscription_status_' . $this->status_transition['from'] . '_to_' . $this->status_transition['to'], $this );
+ if ( ! empty( $status_transition['from'] ) ) {
+ $transition_note = sprintf(
+ /* translators: 1: old subscription status 2: new subscription status */
+ __( 'Status changed from %1$s to %2$s.', 'woocommerce-subscriptions' ),
+ wcs_get_subscription_status_name( $status_transition['from'] ),
+ wcs_get_subscription_status_name( $status_transition['to'] )
+ );
- // Trigger a hook with params we want
- do_action( 'woocommerce_subscription_status_updated', $this, $this->status_transition['to'], $this->status_transition['from'] );
+ do_action( "woocommerce_subscription_status_{$status_transition['from']}_to_{$status_transition['to']}", $this );
- // Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes
- do_action( 'woocommerce_subscription_status_changed', $this->get_id(), $this->status_transition['from'], $this->status_transition['to'], $this );
+ // Trigger a hook with params we want.
+ do_action( 'woocommerce_subscription_status_updated', $this, $status_transition['to'], $status_transition['from'] );
+ // Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes.
+ do_action( 'woocommerce_subscription_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this );
} else {
/* translators: %s: new order status */
- $transition_note = sprintf( __( 'Status set to %s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $this->status_transition['to'] ) );
+ $transition_note = sprintf( __( 'Status set to %s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $status_transition['to'] ) );
}
- // Note the transition occured
- $this->add_order_note( trim( $this->status_transition['note'] . ' ' . $transition_note ), 0, $this->status_transition['manual'] );
-
- // This has ran, so reset status transition variable
- $this->status_transition = false;
+ // Note the transition occurred.
+ $this->add_order_note( trim( "{$status_transition['note']} {$transition_note}" ), 0, $status_transition['manual'] );
+ } catch ( Exception $e ) {
+ $logger = wc_get_logger();
+ $logger->error(
+ sprintf( 'Status transition of subscription #%d errored!', $this->get_id() ),
+ array(
+ 'order' => $this,
+ 'error' => $e,
+ )
+ );
+ $this->add_order_note( __( 'Error during subscription status transition.', 'woocommerce-subscriptions' ) . ' ' . $e->getMessage() );
}
+
+ // This has run, so reset status transition variable
+ $this->status_transition = false;
}
/**
@@ -1799,10 +1831,13 @@ class WC_Subscription extends WC_Order {
$related_orders = array();
foreach ( $order_types as $order_type ) {
-
$related_orders_for_order_type = array();
foreach ( $this->get_related_order_ids( $order_type ) as $order_id ) {
- $related_orders_for_order_type[ $order_id ] = ( 'all' == $return_fields ) ? wc_get_order( $order_id ) : $order_id;
+ if ( 'all' === $return_fields && $order = wc_get_order( $order_id ) ) {
+ $related_orders_for_order_type[ $order_id ] = $order;
+ } elseif ( 'ids' === $return_fields ) {
+ $related_orders_for_order_type[ $order_id ] = $order_id;
+ }
}
$related_orders += apply_filters( 'woocommerce_subscription_related_orders', $related_orders_for_order_type, $this, $return_fields, $order_type );
@@ -1886,9 +1921,11 @@ class WC_Subscription extends WC_Order {
/**
* Determine how the payment method should be displayed for a subscription.
*
+ * @param string $context The context the payment method is being displayed in. Can be 'admin' or 'customer'. Default 'admin'.
+ *
* @since 2.0
*/
- public function get_payment_method_to_display() {
+ public function get_payment_method_to_display( $context = 'admin' ) {
if ( $this->is_manual() ) {
@@ -1899,14 +1936,25 @@ class WC_Subscription extends WC_Order {
$payment_method_to_display = $payment_gateway->get_title();
- // Fallback to the title of the payment method when the subscripion was created
+ // Fallback to the title of the payment method when the subscription was created
} else {
$payment_method_to_display = $this->get_payment_method_title();
}
- return apply_filters( 'woocommerce_subscription_payment_method_to_display', $payment_method_to_display, $this );
+ $payment_method_to_display = apply_filters( 'woocommerce_subscription_payment_method_to_display', $payment_method_to_display, $this, $context );
+
+ if ( 'customer' === $context ) {
+ $payment_method_to_display = sprintf( __( 'Via %s', 'woocommerce-subscriptions' ), $payment_method_to_display );
+
+ // Only filter the result for non-manual subscriptions.
+ if ( ! $this->is_manual() ) {
+ $payment_method_to_display = apply_filters( 'woocommerce_my_subscriptions_payment_method', $payment_method_to_display, $this );
+ }
+ }
+
+ return $payment_method_to_display;
}
/**
@@ -2029,6 +2077,16 @@ class WC_Subscription extends WC_Order {
return $has_product;
}
+ /**
+ * Check if the subscription has a payment gateway.
+ *
+ * @since 2.5.0
+ * @return bool
+ */
+ public function has_payment_gateway() {
+ return (bool) wc_get_payment_gateway_by_order( $this );
+ }
+
/**
* The total sign-up fee for the subscription if any.
*
@@ -2355,7 +2413,17 @@ class WC_Subscription extends WC_Order {
}
/**
- * Get the subscription's payment method meta.
+ * Generates a URL to add or change the subscription's payment method from the my account page.
+ *
+ * @return string
+ * @since 2.5.0
+ */
+ public function get_change_payment_method_url() {
+ $change_payment_method_url = wc_get_endpoint_url( 'subscription-payment-method', $this->get_id(), wc_get_page_permalink( 'myaccount' ) );
+ return apply_filters( 'wcs_get_change_payment_method_url', $change_payment_method_url, $this->get_id() );
+ }
+
+ /* Get the subscription's payment method meta.
*
* @since 2.4.3
* @return array The subscription's payment meta in the format returned by the woocommerce_subscription_payment_meta filter.
@@ -2396,6 +2464,23 @@ class WC_Subscription extends WC_Order {
return null;
}
+ /**
+ * Get totals for display on pages and in emails.
+ *
+ * @param mixed $tax_display Excl or incl tax display mode.
+ * @return array
+ */
+ public function get_order_item_totals( $tax_display = '' ) {
+ $total_rows = parent::get_order_item_totals( $tax_display );
+
+ // Use get_payment_method_to_display() as it displays "Manual Renewal" for manual subscriptions.
+ if ( isset( $total_rows['payment_method'] ) ) {
+ $total_rows['payment_method']['value'] = $this->get_payment_method_to_display( 'customer' );
+ }
+
+ return apply_filters( 'woocommerce_get_subscription_item_totals', $total_rows, $this, $tax_display );
+ }
+
/************************
* Deprecated Functions *
************************/
diff --git a/includes/class-wc-subscriptions-cart.php b/includes/class-wc-subscriptions-cart.php
index e1a3e82..b85c400 100755
--- a/includes/class-wc-subscriptions-cart.php
+++ b/includes/class-wc-subscriptions-cart.php
@@ -80,13 +80,6 @@ class WC_Subscriptions_Cart {
// Make sure cart product prices correctly include/exclude taxes
add_filter( 'woocommerce_cart_product_price', __CLASS__ . '::cart_product_price' , 10, 2 );
- // Make sure cart totals are calculated when setting up the cart widget
- add_action( 'wc_ajax_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments' , 1 );
- add_action( 'wp_ajax_woocommerce_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments', 1 );
- add_action( 'wp_ajax_nopriv_woocommerce_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments', 1, 1 );
-
- add_action( 'woocommerce_ajax_added_to_cart', __CLASS__ . '::pre_get_refreshed_fragments', 1, 1 );
-
// Display grouped recurring amounts after order totals on the cart/checkout pages
add_action( 'woocommerce_cart_totals_after_order_total', __CLASS__ . '::display_recurring_totals' );
add_action( 'woocommerce_review_order_after_order_total', __CLASS__ . '::display_recurring_totals' );
@@ -797,6 +790,17 @@ class WC_Subscriptions_Cart {
return $cart_contains_free_trial;
}
+ /**
+ * Checks to see if payment method is required on a subscription product with a $0 initial payment.
+ *
+ * @since 2.5.0
+ */
+ public static function zero_initial_payment_requires_payment() {
+
+ return 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_zero_initial_payment_requires_payment', 'no' );
+
+ }
+
/**
* Gets the cart calculation type flag
*
@@ -860,12 +864,28 @@ class WC_Subscriptions_Cart {
* @return bool
*/
public static function cart_needs_payment( $needs_payment, $cart ) {
- if ( false === $needs_payment && self::cart_contains_subscription() && $cart->total == 0 && false === WC_Subscriptions_Switcher::cart_contains_switches() && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) {
- $recurring_total = 0;
- $is_one_period = true;
- $contains_synced = false;
- $contains_expiring_limited_coupon = false;
+ // Skip checks if needs payment is already set or cart total not 0.
+ if ( false !== $needs_payment || 0 != $cart->total ) {
+ return $needs_payment;
+ }
+
+ // Skip checks if new $0 initial payments don't require a payment method or cart has no subscriptions.
+ if ( ! self::zero_initial_payment_requires_payment() || ! self::cart_contains_subscription() ) {
+ return $needs_payment;
+ }
+
+ // Skip checks if cart contains subscription switches or automatic payments are disabled.
+ if ( false !== WC_Subscriptions_Switcher::cart_contains_switches() || 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) {
+ return $needs_payment;
+ }
+
+ $recurring_total = 0;
+ $is_one_period = true;
+ $contains_synced = false;
+ $contains_expiring_limited_coupon = false;
+
+ if ( ! empty( WC()->cart->recurring_carts ) ) {
foreach ( WC()->cart->recurring_carts as $recurring_cart ) {
$recurring_total += $recurring_cart->total;
$subscription_length = wcs_cart_pluck( $recurring_cart, 'subscription_length' );
@@ -876,12 +896,12 @@ class WC_Subscriptions_Cart {
$is_one_period = false;
}
}
+ }
- $has_trial = self::cart_contains_free_trial();
+ $needs_trial_payment = self::cart_contains_free_trial();
- if ( $contains_expiring_limited_coupon || $recurring_total > 0 && ( ! $is_one_period || $has_trial || $contains_synced ) ) {
- $needs_payment = true;
- }
+ if ( $contains_expiring_limited_coupon || $recurring_total > 0 && ( ! $is_one_period || $needs_trial_payment || $contains_synced ) ) {
+ $needs_payment = true;
}
return $needs_payment;
@@ -963,19 +983,6 @@ class WC_Subscriptions_Cart {
return $price;
}
- /**
- * Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method
- * so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string.
- *
- * @since 1.5.11
- */
- public static function pre_get_refreshed_fragments() {
- if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) {
- define( 'WOOCOMMERCE_CART', true );
- WC()->cart->calculate_totals();
- }
- }
-
/**
* Display the recurring totals for items in the cart
*
@@ -1343,6 +1350,21 @@ class WC_Subscriptions_Cart {
/* Deprecated */
+ /**
+ * Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method
+ * so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string.
+ *
+ * @since 1.5.11
+ * @deprecated 2.5.0
+ */
+ public static function pre_get_refreshed_fragments() {
+ wcs_deprecated_function( __METHOD__, '2.5.0' );
+ if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) {
+ define( 'WOOCOMMERCE_CART', true );
+ WC()->cart->calculate_totals();
+ }
+ }
+
/**
* Checks the cart to see if it contains a subscription product renewal.
*
diff --git a/includes/class-wc-subscriptions-change-payment-gateway.php b/includes/class-wc-subscriptions-change-payment-gateway.php
index ff166e7..5eb9a08 100755
--- a/includes/class-wc-subscriptions-change-payment-gateway.php
+++ b/includes/class-wc-subscriptions-change-payment-gateway.php
@@ -67,6 +67,10 @@ class WC_Subscriptions_Change_Payment_Gateway {
// Maybe filter subscriptions_needs_payment to return false when processing change-payment-gateway requests
add_filter( 'woocommerce_subscription_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 1 );
+
+ // Display a login form if the customer is requesting to change their payment method but aren't logged in.
+ add_filter( 'the_content', array( __CLASS__, 'maybe_request_log_in' ) );
+
}
/**
@@ -310,11 +314,16 @@ class WC_Subscriptions_Change_Payment_Gateway {
if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) {
+ if ( $subscription->has_payment_gateway() && wc_get_payment_gateway_by_order( $subscription )->supports( 'subscriptions' ) ) {
+ $action_name = _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' );
+ } else {
+ $action_name = _x( 'Add Payment', 'label on button, imperative', 'woocommerce-subscriptions' );
+ }
+
$actions['change_payment_method'] = array(
'url' => wp_nonce_url( add_query_arg( array( 'change_payment_method' => $subscription->get_id() ), $subscription->get_checkout_payment_url() ) ),
- 'name' => _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' ),
+ 'name' => $action_name,
);
-
}
return $actions;
@@ -331,81 +340,202 @@ class WC_Subscriptions_Change_Payment_Gateway {
*/
public static function change_payment_method_via_pay_shortcode() {
- if ( isset( $_POST['_wcsnonce'] ) && wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) ) {
+ if ( ! isset( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) ) {
+ return;
+ }
- $subscription = wcs_get_subscription( absint( $_POST['woocommerce_change_payment'] ) );
+ $subscription_id = absint( $_POST['woocommerce_change_payment'] );
+ $subscription = wcs_get_subscription( $subscription_id );
- do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription );
+ do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription );
- ob_start();
+ ob_start();
- if ( $subscription->get_order_key() == $_GET['key'] ) {
+ if ( $subscription->get_order_key() == $_GET['key'] ) {
- $subscription_billing_country = $subscription->get_billing_country();
- $subscription_billing_state = $subscription->get_billing_state();
- $subscription_billing_postcode = $subscription->get_billing_postcode();
- $subscription_billing_city = $subscription->get_billing_postcode();
+ $subscription_billing_country = $subscription->get_billing_country();
+ $subscription_billing_state = $subscription->get_billing_state();
+ $subscription_billing_postcode = $subscription->get_billing_postcode();
+ $subscription_billing_city = $subscription->get_billing_postcode();
- // Set customer location to order location
- if ( $subscription_billing_country ) {
- $setter = is_callable( array( WC()->customer, 'set_billing_country' ) ) ? 'set_billing_country' : 'set_country';
- WC()->customer->$setter( $subscription_billing_country );
- }
- if ( $subscription_billing_state ) {
- $setter = is_callable( array( WC()->customer, 'set_billing_state' ) ) ? 'set_billing_state' : 'set_state';
- WC()->customer->$setter( $subscription_billing_state );
- }
- if ( $subscription_billing_postcode ) {
- $setter = is_callable( array( WC()->customer, 'set_billing_postcode' ) ) ? 'set_billing_postcode' : 'set_postcode';
- WC()->customer->$setter( $subscription_billing_postcode );
- }
- if ( $subscription_billing_city ) {
- $setter = is_callable( array( WC()->customer, 'set_billing_city' ) ) ? 'set_billing_city' : 'set_city';
- WC()->customer->$setter( $subscription_billing_city );
+ // Set customer location to order location
+ if ( $subscription_billing_country ) {
+ $setter = is_callable( array( WC()->customer, 'set_billing_country' ) ) ? 'set_billing_country' : 'set_country';
+ WC()->customer->$setter( $subscription_billing_country );
+ }
+ if ( $subscription_billing_state ) {
+ $setter = is_callable( array( WC()->customer, 'set_billing_state' ) ) ? 'set_billing_state' : 'set_state';
+ WC()->customer->$setter( $subscription_billing_state );
+ }
+ if ( $subscription_billing_postcode ) {
+ $setter = is_callable( array( WC()->customer, 'set_billing_postcode' ) ) ? 'set_billing_postcode' : 'set_postcode';
+ WC()->customer->$setter( $subscription_billing_postcode );
+ }
+ if ( $subscription_billing_city ) {
+ $setter = is_callable( array( WC()->customer, 'set_billing_city' ) ) ? 'set_billing_city' : 'set_city';
+ WC()->customer->$setter( $subscription_billing_city );
+ }
+
+ // Update payment method
+ $new_payment_method = wc_clean( $_POST['payment_method'] );
+ $notice = $subscription->has_payment_gateway() ? __( 'Payment method updated.', 'woocommerce-subscriptions' ) : __( 'Payment method added.', 'woocommerce-subscriptions' );
+
+ // Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed
+ if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) {
+ self::update_payment_method( $subscription, $new_payment_method );
+ }
+
+ $available_gateways = WC()->payment_gateways->get_available_payment_gateways();
+
+ // Validate
+ $available_gateways[ $new_payment_method ]->validate_fields();
+
+ // Process payment for the new method (with a $0 order total)
+ if ( wc_notice_count( 'error' ) == 0 ) {
+
+ $result = $available_gateways[ $new_payment_method ]->process_payment( $subscription->get_id() );
+
+ if ( 'success' == $result['result'] && wc_get_page_permalink( 'myaccount' ) == $result['redirect'] ) {
+ $result['redirect'] = $subscription->get_view_order_url();
}
- // Update payment method
- $new_payment_method = wc_clean( $_POST['payment_method'] );
+ $result = apply_filters( 'woocommerce_subscriptions_process_payment_for_change_method_via_pay_shortcode', $result, $subscription );
- // Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed
- if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) {
- self::update_payment_method( $subscription, $new_payment_method );
+ if ( 'success' != $result['result'] ) {
+ return;
}
- $available_gateways = WC()->payment_gateways->get_available_payment_gateways();
+ $subscription->set_requires_manual_renewal( false );
+ $subscription->save();
- // Validate
- $available_gateways[ $new_payment_method ]->validate_fields();
-
- // Process payment for the new method (with a $0 order total)
- if ( wc_notice_count( 'error' ) == 0 ) {
-
- $result = $available_gateways[ $new_payment_method ]->process_payment( $subscription->get_id() );
-
- if ( 'success' == $result['result'] && wc_get_page_permalink( 'myaccount' ) == $result['redirect'] ) {
- $result['redirect'] = $subscription->get_view_order_url();
- }
-
- $result = apply_filters( 'woocommerce_subscriptions_process_payment_for_change_method_via_pay_shortcode', $result, $subscription );
-
- // Redirect to success/confirmation/payment page
- if ( 'success' == $result['result'] ) {
- wc_add_notice( __( 'Payment method updated.', 'woocommerce-subscriptions' ), 'success' );
- wp_redirect( $result['redirect'] );
- exit;
+ // Does the customer want all current subscriptions to be updated to this payment method?
+ if (
+ isset( $_POST['update_all_subscriptions_payment_method'] )
+ && $_POST['update_all_subscriptions_payment_method']
+ && WC_Subscriptions_Change_Payment_Gateway::can_update_all_subscription_payment_methods( $available_gateways[ $new_payment_method ], $subscription )
+ ) {
+ // Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed
+ if ( ! apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) {
+ $subscription->update_meta_data( '_delayed_update_payment_method_all', $new_payment_method );
+ $subscription->save();
+ $notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-subscriptions' );
+ } elseif ( self::update_all_payment_methods_from_subscription( $subscription, $new_payment_method ) ) {
+ $notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-subscriptions' );
}
}
+
+ // Redirect to success/confirmation/payment page
+ wc_add_notice( $notice );
+ wp_redirect( $result['redirect'] );
+ exit;
}
}
+
+ ob_get_clean();
+ }
+
+ /**
+ * Update the recurring payment method on all current subscriptions to the payment method on this subscription.
+ *
+ * @param WC_Subscription $subscription An instance of a WC_Subscription object.
+ * @param string $new_payment_method The ID of the new payment method.
+ * @return bool Were other subscriptions updated.
+ * @since 2.5.0
+ */
+ public static function update_all_payment_methods_from_subscription( $subscription, $new_payment_method ) {
+
+ // Require the delayed payment update method to match the current gateway if it is set
+ if ( self::will_subscription_update_all_payment_methods( $subscription ) ) {
+ if ( $subscription->get_meta( '_delayed_update_payment_method_all' ) != $new_payment_method ) {
+ return false;
+ }
+
+ $subscription->delete_meta_data( '_delayed_update_payment_method_all' );
+ $subscription->save_meta_data();
+ }
+
+ $payment_meta_table = WCS_Payment_tokens::get_subscription_payment_meta( $subscription, $new_payment_method );
+ if ( ! is_array( $payment_meta_table ) ) {
+ return false;
+ }
+
+ $subscription_ids = WCS_Customer_Store::instance()->get_users_subscription_ids( $subscription->get_customer_id() );
+
+ foreach ( $subscription_ids as $subscription_id ) {
+ // Skip the subscription providing the new payment meta.
+ if ( $subscription->get_id() == $subscription_id ) {
+ continue;
+ }
+
+ $user_subscription = wcs_get_subscription( $subscription_id );
+ // Skip if subscription's current payment method is not supported
+ if ( ! $user_subscription->payment_method_supports( 'subscription_cancellation' ) ) {
+ continue;
+ }
+
+ // Skip if there are no remaining payments or the subscription is not current.
+ if ( $user_subscription->get_time( 'next_payment' ) <= 0 || ! $user_subscription->has_status( array( 'active', 'on-hold' ) ) ) {
+ continue;
+ }
+
+ self::update_payment_method( $user_subscription, $new_payment_method, $payment_meta_table );
+
+ $user_subscription->set_requires_manual_renewal( false );
+ $user_subscription->save();
+ }
+
+ return true;
+ }
+
+ /**
+ * Check whether a payment method supports updating all current subscriptions' payment method.
+ *
+ * @param WC_Payment_Gateway $gateway The payment gateway to check.
+ * @param WC_Subscription $subscription An instance of a WC_Subscription object.
+ * @return bool Gateway supports updating all current subscriptions.
+ * @since 2.5.0
+ */
+ public static function can_update_all_subscription_payment_methods( $gateway, $subscription ) {
+
+ if ( $gateway->supports( 'subscription_payment_method_delayed_change' ) ) {
+ return true;
+ }
+
+ if ( ! $gateway->supports( 'subscription_payment_method_change_admin' ) ) {
+ return false;
+ }
+
+ if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $gateway->id, $subscription ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check whether a subscription will update all current subscriptions' payment method.
+ *
+ * @param WC_Subscription $subscription An instance of a WC_Subscription object.
+ * @return bool Subscription will update all current subscriptions.
+ * @since 2.5.0
+ */
+ public static function will_subscription_update_all_payment_methods( $subscription ) {
+ if ( ! wcs_is_subscription( $subscription ) ) {
+ return false;
+ }
+
+ return (bool) $subscription->get_meta( '_delayed_update_payment_method_all' );
}
/**
* Update the recurring payment method on a subscription order.
*
- * @param array $available_gateways The payment gateways which are currently being allowed.
+ * @param WC_Subscription $subscription An instance of a WC_Subscription object.
+ * @param string $new_payment_method The ID of the new payment method.
+ * @param array $new_payment_method_meta The meta for the new payment method. Optional. Default false.
* @since 1.4
*/
- public static function update_payment_method( $subscription, $new_payment_method ) {
+ public static function update_payment_method( $subscription, $new_payment_method, $new_payment_method_meta = false ) {
$old_payment_method = $subscription->get_payment_method();
$old_payment_method_title = $subscription->get_payment_method_title();
@@ -417,18 +547,12 @@ class WC_Subscriptions_Change_Payment_Gateway {
WC_Subscriptions_Payment_Gateways::trigger_gateway_status_updated_hook( $subscription, 'cancelled' );
// Update meta
- update_post_meta( $subscription->get_id(), '_old_payment_method', $old_payment_method );
- update_post_meta( $subscription->get_id(), '_payment_method', $new_payment_method );
-
if ( isset( $available_gateways[ $new_payment_method ] ) ) {
$new_payment_method_title = $available_gateways[ $new_payment_method ]->get_title();
} else {
$new_payment_method_title = '';
}
- update_post_meta( $subscription->get_id(), '_old_payment_method_title', $old_payment_method_title );
- update_post_meta( $subscription->get_id(), '_payment_method_title', $new_payment_method_title );
-
if ( empty( $old_payment_method_title ) ) {
$old_payment_method_title = $old_payment_method;
}
@@ -437,12 +561,21 @@ class WC_Subscriptions_Change_Payment_Gateway {
$new_payment_method_title = $new_payment_method;
}
+ $subscription->update_meta_data( '_old_payment_method', $old_payment_method );
+ $subscription->update_meta_data( '_old_payment_method_title', $old_payment_method_title );
+ $subscription->set_payment_method( $new_payment_method, $new_payment_method_meta );
+ $subscription->set_payment_method_title( $new_payment_method_title );
+
// Log change on order
$subscription->add_order_note( sprintf( _x( 'Payment method changed from "%1$s" to "%2$s" by the subscriber from their account page.', '%1$s: old payment title, %2$s: new payment title', 'woocommerce-subscriptions' ), $old_payment_method_title, $new_payment_method_title ) );
+ $subscription->save();
+
do_action( 'woocommerce_subscription_payment_method_updated', $subscription, $new_payment_method, $old_payment_method );
do_action( 'woocommerce_subscription_payment_method_updated_to_' . $new_payment_method, $subscription, $old_payment_method );
- do_action( 'woocommerce_subscription_payment_method_updated_from_' . $old_payment_method, $subscription, $new_payment_method );
+ if ( $old_payment_method ) {
+ do_action( 'woocommerce_subscription_payment_method_updated_from_' . $old_payment_method, $subscription, $new_payment_method );
+ }
}
/**
@@ -552,26 +685,39 @@ class WC_Subscriptions_Change_Payment_Gateway {
* For the recurring payment method to be changeable, the subscription must be active, have future (automatic) payments
* and use a payment gateway which allows the subscription to be cancelled.
*
- * @param bool $subscription_can_be_changed Flag of whether the subscription can be changed to
- * @param string $new_status_or_meta The status or meta data you want to change th subscription to. Can be 'active', 'on-hold', 'cancelled', 'expired', 'trash', 'deleted', 'failed', 'new-payment-date' or some other value attached to the 'woocommerce_can_subscription_be_changed_to' filter.
- * @param object $args Set of values used in @see WC_Subscriptions_Manager::can_subscription_be_changed_to() for determining if a subscription can be changes, include:
- * 'subscription_key' string A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
- * 'subscription' array Subscription of the form returned by @see WC_Subscriptions_Manager::get_subscription()
- * 'user_id' int The ID of the subscriber.
- * 'order' WC_Order The order which recorded the successful payment (to make up for the failed automatic payment).
- * 'payment_gateway' WC_Payment_Gateway The subscription's recurring payment gateway
- * 'order_uses_manual_payments' bool A boolean flag indicating whether the subscription requires manual renewal payment.
+ * @param bool $subscription_can_be_changed Flag of whether the subscription can be changed.
+ * @param WC_Subscription $subscription The subscription to check.
+ * @return bool Flag indicating whether the subscription payment method can be updated.
* @since 1.4
*/
public static function can_subscription_be_updated_to_new_payment_method( $subscription_can_be_changed, $subscription ) {
- if ( WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_customer' ) && $subscription->get_time( 'next_payment' ) > 0 && ! $subscription->is_manual() && $subscription->payment_method_supports( 'subscription_cancellation' ) && $subscription->has_status( 'active' ) ) {
- $subscription_can_be_changed = true;
- } else {
- $subscription_can_be_changed = false;
+ // Don't allow if automatic payments are disabled and the toggle is also disabled.
+ if ( 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) && ! WCS_My_Account_Auto_Renew_Toggle::is_enabled() ) {
+ return false;
}
- return $subscription_can_be_changed;
+ // If there's no recurring payment, there's no need to add or update the payment method.
+ if ( $subscription->get_total() == 0 ) {
+ return false;
+ }
+
+ // Don't allow if no gateways support changing methods.
+ if ( ! WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_customer' ) ) {
+ return false;
+ }
+
+ // Don't allow if there are no remaining payments or the subscription is not active.
+ if ( $subscription->get_time( 'next_payment' ) <= 0 || ! $subscription->has_status( 'active' ) ) {
+ return false;
+ }
+
+ // Don't allow on subscription that doesn't support changing methods.
+ if ( ! $subscription->payment_method_supports( 'subscription_cancellation' ) ) {
+ return false;
+ }
+
+ return true;
}
/**
@@ -583,8 +729,22 @@ class WC_Subscriptions_Change_Payment_Gateway {
*/
public static function change_payment_method_page_title( $title ) {
- if ( is_main_query() && in_the_loop() && is_page() && is_checkout_pay_page() && self::$is_request_to_change_payment ) {
+ global $wp;
+
+ // Skip if not on checkout pay page or not a payment change request.
+ if ( ! self::$is_request_to_change_payment || ! is_main_query() || ! in_the_loop() || ! is_page() || ! is_checkout_pay_page() ) {
+ return $title;
+ }
+
+ $subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
+ if ( ! $subscription ) {
+ return $title;
+ }
+
+ if ( $subscription->has_payment_gateway() ) {
$title = _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' );
+ } else {
+ $title = _x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' );
}
return $title;
@@ -617,10 +777,17 @@ class WC_Subscriptions_Change_Payment_Gateway {
esc_url( $subscription->get_view_order_url() ),
);
- $crumbs[3] = array(
- _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ),
- '',
- );
+ if ( $subscription->has_payment_gateway() ) {
+ $crumbs[3] = array(
+ _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ),
+ '',
+ );
+ } else {
+ $crumbs[3] = array(
+ _x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ),
+ '',
+ );
+ }
}
return $crumbs;
@@ -645,6 +812,37 @@ class WC_Subscriptions_Change_Payment_Gateway {
return $needs_payment;
}
+ /**
+ * Display a login form on the change payment method page if the customer isn't logged in.
+ *
+ * @param string $content The default HTML page content.
+ * @return string $content.
+ * @since 2.5.0
+ */
+ public static function maybe_request_log_in( $content ) {
+ global $wp;
+
+ if ( ! self::$is_request_to_change_payment || is_user_logged_in() || ! isset( $wp->query_vars['order-pay'] ) ) {
+ return $content;
+ }
+
+ $subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
+
+ if ( $subscription ) {
+ wc_add_notice( __( 'Please log in to your account below to choose a new payment method for your subscription.', 'woocommerce-subscriptions' ), 'notice' );
+
+ ob_start();
+ woocommerce_login_form( array(
+ 'redirect' => $subscription->get_change_payment_method_url(),
+ 'message' => wc_print_notices( true ),
+ ) );
+
+ $content = ob_get_clean();
+ }
+
+ return $content;
+ }
+
/** Deprecated Functions **/
/**
diff --git a/includes/class-wc-subscriptions-checkout.php b/includes/class-wc-subscriptions-checkout.php
index 51c5f6f..79ea7f4 100755
--- a/includes/class-wc-subscriptions-checkout.php
+++ b/includes/class-wc-subscriptions-checkout.php
@@ -48,10 +48,10 @@ class WC_Subscriptions_Checkout {
public static function attach_dependant_hooks() {
// Make sure guest checkout is not enabled in option param passed to WC JS
if ( WC_Subscriptions::is_woocommerce_pre( '3.3' ) ) {
- add_filter( 'woocommerce_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 );
- add_filter( 'wc_checkout_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 );
+ add_filter( 'woocommerce_params', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 1 );
+ add_filter( 'wc_checkout_params', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 1 );
} else {
- add_filter( 'woocommerce_get_script_data', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 2 );
+ add_filter( 'woocommerce_get_script_data', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 2 );
}
}
@@ -455,11 +455,18 @@ class WC_Subscriptions_Checkout {
* Also make sure the guest checkout option value passed to the woocommerce.js forces registration.
* Otherwise the registration form is hidden by woocommerce.js.
*
- * @since 1.1
+ * @param string $handle Default empty string ('').
+ * @param array $woocommerce_params
+ *
+ * @since 2.5.3
+ * @return array
*/
- public static function filter_woocommerce_script_paramaters( $woocommerce_params, $handle = '' ) {
+ public static function filter_woocommerce_script_parameters( $woocommerce_params, $handle = '' ) {
// WC 3.3+ deprecates handle-specific filters in favor of 'woocommerce_get_script_data'.
- if ( 'woocommerce_get_script_data' === current_filter() && ! in_array( $handle, array( 'woocommerce', 'wc-checkout' ) ) ) {
+ if ( 'woocommerce_get_script_data' === current_filter() && ! in_array( $handle, array(
+ 'woocommerce',
+ 'wc-checkout',
+ ) ) ) {
return $woocommerce_params;
}
@@ -470,6 +477,19 @@ class WC_Subscriptions_Checkout {
return $woocommerce_params;
}
+ /**
+ * Also make sure the guest checkout option value passed to the woocommerce.js forces registration.
+ * Otherwise the registration form is hidden by woocommerce.js.
+ *
+ * @since 1.1
+ * @deprecated 2.5.3
+ */
+ public static function filter_woocommerce_script_paramaters( $woocommerce_params, $handle = '' ) {
+ wcs_deprecated_function( __METHOD__, '2.5.3', 'WC_Subscriptions_Admin::filter_woocommerce_script_parameters( $woocommerce_params, $handle )' );
+
+ return self::filter_woocommerce_script_parameters( $woocommerce_params, $handle );
+ }
+
/**
* During the checkout process, force registration when the cart contains a subscription.
*
diff --git a/includes/class-wc-subscriptions-manager.php b/includes/class-wc-subscriptions-manager.php
index 86e216d..9fbeda1 100755
--- a/includes/class-wc-subscriptions-manager.php
+++ b/includes/class-wc-subscriptions-manager.php
@@ -1195,7 +1195,7 @@ class WC_Subscriptions_Manager {
}
/**
- * Returns the number of completed payments for a given subscription (including the intial payment).
+ * Returns the number of completed payments for a given subscription (including the initial payment).
*
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @param int $user_id The ID of the user who owns the subscriptions. Although this parameter is optional, if you have the User ID you should pass it to improve performance.
diff --git a/includes/class-wc-subscriptions-product.php b/includes/class-wc-subscriptions-product.php
index b12dd56..83160f4 100755
--- a/includes/class-wc-subscriptions-product.php
+++ b/includes/class-wc-subscriptions-product.php
@@ -409,7 +409,9 @@ class WC_Subscriptions_Product {
$sale_price = self::get_sale_price( $product );
$active_price = ( $subscription_price ) ? $subscription_price : self::get_regular_price( $product );
- if ( $product->is_on_sale() && $subscription_price > $sale_price ) {
+ // Ensure that $sale_price is non-empty because other plugins can use woocommerce_product_is_on_sale filter to
+ // forcefully set a product's is_on_sale flag (like Dynamic Pricing )
+ if ( $product->is_on_sale() && '' !== $sale_price && $subscription_price > $sale_price ) {
$active_price = $sale_price;
}
@@ -777,11 +779,13 @@ class WC_Subscriptions_Product {
* @since 2.2.0
*/
public static function needs_one_time_shipping( $product ) {
- $product = self::maybe_get_product_instance( $product );
+ $product = self::maybe_get_product_instance( $product );
+ $variation = null;
if ( $product && $product->is_type( 'variation' ) && is_callable( array( $product, 'get_parent_id' ) ) ) {
- $product = self::maybe_get_product_instance( $product->get_parent_id() );
+ $variation = $product;
+ $product = self::maybe_get_product_instance( $product->get_parent_id() );
}
- return apply_filters( 'woocommerce_subscriptions_product_needs_one_time_shipping', 'yes' === self::get_meta_data( $product, 'subscription_one_time_shipping', 'no' ), $product );
+ return apply_filters( 'woocommerce_subscriptions_product_needs_one_time_shipping', 'yes' === self::get_meta_data( $product, 'subscription_one_time_shipping', 'no' ), $product, $variation );
}
/**
diff --git a/includes/class-wc-subscriptions-switcher.php b/includes/class-wc-subscriptions-switcher.php
index 7cd7fff..50d416c 100755
--- a/includes/class-wc-subscriptions-switcher.php
+++ b/includes/class-wc-subscriptions-switcher.php
@@ -1771,7 +1771,7 @@ class WC_Subscriptions_Switcher {
* @since 2.0
*/
public static function remove_print_switch_link() {
- remove_filter( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10 );
+ remove_action( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10 );
}
/**
@@ -1780,7 +1780,7 @@ class WC_Subscriptions_Switcher {
* @since 2.0
*/
public static function add_print_switch_link( $table_content ) {
- add_filter( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10, 3 );
+ add_action( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10, 3 );
return $table_content;
}
diff --git a/includes/class-wc-subscriptions-synchroniser.php b/includes/class-wc-subscriptions-synchroniser.php
index 6d6b668..94f728d 100755
--- a/includes/class-wc-subscriptions-synchroniser.php
+++ b/includes/class-wc-subscriptions-synchroniser.php
@@ -919,7 +919,10 @@ class WC_Subscriptions_Synchroniser {
add_filter( 'woocommerce_subscriptions_product_trial_expiration_date', __METHOD__, 10, 2 ); // avoid infinite loop
// First make sure the day is in the past so that we don't end up jumping a month or year because of a few hours difference between now and the billing date
- if ( $trial_expiration_timestamp > $first_payment_timestamp && gmdate( 'Ymd', $first_payment_timestamp ) == gmdate( 'Ymd', $trial_expiration_timestamp ) ) {
+ // Use site time to check if the trial expiration and first payment fall on the same day
+ $site_offset = get_option( 'gmt_offset' ) * 3600;
+
+ if ( $trial_expiration_timestamp > $first_payment_timestamp && gmdate( 'Ymd', $first_payment_timestamp + $site_offset ) === gmdate( 'Ymd', $trial_expiration_timestamp + $site_offset ) ) {
$trial_expiration_date = date( 'Y-m-d H:i:s', $first_payment_timestamp );
}
}
@@ -1097,9 +1100,9 @@ class WC_Subscriptions_Synchroniser {
$subscription = wcs_get_subscription( $post_id );
foreach ( $subscription->get_items() as $item ) {
- $product_id = wcs_get_canonical_product_id( $item );
+ $product = $item->get_product();
- if ( self::is_product_synced( $product_id ) ) {
+ if ( self::is_product_synced( $product ) ) {
update_post_meta( $subscription->get_id(), '_contains_synced_subscription', 'true' );
break;
}
@@ -1182,10 +1185,10 @@ class WC_Subscriptions_Synchroniser {
* @since 2.2.3
*/
public static function maybe_add_meta_for_new_line_item( $item_id, $item, $subscription_id ) {
- if ( is_callable( array( $item, 'get_product_id' ) ) ) {
- $product_id = wcs_get_canonical_product_id( $item );
+ if ( is_callable( array( $item, 'get_product' ) ) ) {
+ $product = $item->get_product();
- if ( self::is_product_synced( $product_id ) ) {
+ if ( self::is_product_synced( $product ) ) {
self::maybe_add_subscription_meta( $subscription_id );
}
}
diff --git a/includes/class-wcs-action-scheduler.php b/includes/class-wcs-action-scheduler.php
index 9ebd1da..fef27de 100755
--- a/includes/class-wcs-action-scheduler.php
+++ b/includes/class-wcs-action-scheduler.php
@@ -73,6 +73,8 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
switch ( $new_status ) {
case 'active' :
+ $this->unschedule_actions( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $this->get_action_args( 'end', $subscription ) );
+
foreach ( $this->action_hooks as $action_hook => $date_type ) {
$event_time = $subscription->get_time( $date_type );
@@ -94,6 +96,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
as_schedule_single_action( $event_time, $action_hook, $action_args );
}
}
+
break;
case 'pending-cancel' :
diff --git a/includes/class-wcs-cart-renewal.php b/includes/class-wcs-cart-renewal.php
index 6af40bb..64a1d35 100755
--- a/includes/class-wcs-cart-renewal.php
+++ b/includes/class-wcs-cart-renewal.php
@@ -941,7 +941,15 @@ class WCS_Cart_Renewal {
*/
protected function set_cart_hash( $order_id ) {
$order = wc_get_order( $order_id );
- wcs_set_objects_property( $order, 'cart_hash', md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ) );
+
+ // Use cart hash generator introduced in WooCommerce 3.6
+ if ( is_callable( array( WC()->cart, 'get_cart_hash' ) ) ) {
+ $cart_hash = WC()->cart->get_cart_hash();
+ } else {
+ $cart_hash = md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total );
+ }
+
+ wcs_set_objects_property( $order, 'cart_hash', $cart_hash );
}
/**
@@ -1361,8 +1369,10 @@ class WCS_Cart_Renewal {
protected function apply_order_coupon( $order, $coupon ) {
$coupon_code = $coupon->get_code();
- // Set order products as the product ids on the coupon
- $coupon->set_product_ids( $this->get_products( $order ) );
+ // Set order products as the product ids on the coupon if the coupon does not already have usage restrictions for some products
+ if ( ! $coupon->get_product_ids() ) {
+ $coupon->set_product_ids( $this->get_products( $order ) );
+ }
// Store the coupon info for later
$this->store_coupon( $order->get_id(), $coupon );
diff --git a/includes/class-wcs-failed-scheduled-action-manager.php b/includes/class-wcs-failed-scheduled-action-manager.php
index d6748ef..294fcdd 100755
--- a/includes/class-wcs-failed-scheduled-action-manager.php
+++ b/includes/class-wcs-failed-scheduled-action-manager.php
@@ -131,13 +131,13 @@ class WCS_Failed_Scheduled_Action_Manager {
) );
$notice->set_actions( array(
array(
- 'name' => __( 'Ignore this error (not recommended)', 'woocommerce-subscriptions' ),
+ 'name' => __( 'Ignore this error', 'woocommerce-subscriptions' ),
'url' => wp_nonce_url( add_query_arg( 'wcs_scheduled_action_timeout_error_notice', 'ignore' ), 'wcs_scheduled_action_timeout_error_notice', '_wcsnonce' ),
'class' => 'button',
),
array(
- 'name' => __( 'Open a ticket', 'woocommerce-subscriptions' ),
- 'url' => 'https://woocommerce.com/my-account/marketplace-ticket-form/',
+ 'name' => __( 'Learn more', 'woocommerce-subscriptions' ),
+ 'url' => 'https://docs.woocommerce.com/document/subscriptions/scheduled-action-errors/',
'class' => 'button button-primary',
),
) );
diff --git a/includes/class-wcs-my-account-auto-renew-toggle.php b/includes/class-wcs-my-account-auto-renew-toggle.php
new file mode 100755
index 0000000..9e4bb3f
--- /dev/null
+++ b/includes/class-wcs-my-account-auto-renew-toggle.php
@@ -0,0 +1,151 @@
+has_status( 'active' ) ) {
+ return false;
+ }
+ // Cannot change to auto-renewal for a subscription with 0 total
+ if ( 0 == $subscription->get_total() ) { // Not using strict comparison intentionally
+ return false;
+ }
+ // Cannot change to auto-renewal for a subscription in the final billing period. No next renewal date.
+ if ( 0 == $subscription->get_date( 'next_payment' ) ) { // Not using strict comparison intentionally
+ return false;
+ }
+ // If it is not a manual subscription, and the payment gateway is PayPal Standard
+ if ( ! $subscription->is_manual() && $subscription->payment_method_supports( 'gateway_scheduled_payments' ) ) {
+ return false;
+ }
+
+ // Looks like changing to auto-renewal is indeed possible
+ return true;
+ }
+
+ /**
+ * Disable auto renewal of subscription
+ *
+ * @since 2.5.0
+ */
+ public static function disable_auto_renew() {
+
+ if ( ! isset( $_POST['subscription_id'] ) ) {
+ return -1;
+ }
+
+ $subscription_id = absint( $_POST['subscription_id'] );
+ check_ajax_referer( "toggle-auto-renew-{$subscription_id}", 'security' );
+
+ $subscription = wcs_get_subscription( $subscription_id );
+
+ if ( $subscription ) {
+ $subscription->set_requires_manual_renewal( true );
+ $subscription->save();
+
+ self::send_ajax_response( $subscription );
+ }
+ }
+
+ /**
+ * Enable auto renewal of subscription
+ *
+ * @since 2.5.0
+ */
+ public static function enable_auto_renew() {
+
+ if ( ! isset( $_POST['subscription_id'] ) ) {
+ return -1;
+ }
+
+ $subscription_id = absint( $_POST['subscription_id'] );
+ check_ajax_referer( "toggle-auto-renew-{$subscription_id}", 'security' );
+
+ $subscription = wcs_get_subscription( $subscription_id );
+
+ if ( wc_get_payment_gateway_by_order( $subscription ) ) {
+ $subscription->set_requires_manual_renewal( false );
+ $subscription->save();
+
+ self::send_ajax_response( $subscription );
+ }
+ }
+
+ /**
+ * Send a response after processing the AJAX request so the page can be updated.
+ *
+ * @param WC_Subscription $subscription
+ */
+ protected static function send_ajax_response( $subscription ) {
+ wp_send_json( array( 'payment_method' => esc_attr( $subscription->get_payment_method_to_display( 'customer' ) ) ) );
+ }
+
+ /**
+ * Add a setting to allow store managers to enable or disable the auto-renewal toggle.
+ *
+ * @param array $settings
+ * @return array
+ * @since 2.5.0
+ */
+ public static function add_setting( $settings ) {
+ WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_subscriptions_turn_off_automatic_payments', array(
+ 'id' => self::$setting_id,
+ 'name' => __( 'Auto Renewal Toggle', 'woocommerce-subscriptions' ),
+ 'desc' => __( 'Display the auto renewal toggle', 'woocommerce-subscriptions' ),
+ 'desc_tip' => __( 'Allow customers to turn on and off automatic renewals from their View Subscription page.', 'woocommerce-subscriptions' ),
+ 'default' => 'no',
+ 'type' => 'checkbox',
+ ) );
+
+ return $settings;
+ }
+
+ /**
+ * Checks if the store has enabled the auto-renewal toggle.
+ *
+ * @return bool true if the toggle is enabled, otherwise false.
+ * @since 2.5.0
+ */
+ public static function is_enabled() {
+ return 'yes' === get_option( self::$setting_id, 'no' );
+ }
+}
diff --git a/includes/class-wcs-my-account-payment-methods.php b/includes/class-wcs-my-account-payment-methods.php
index 2c4a815..14b6615 100755
--- a/includes/class-wcs-my-account-payment-methods.php
+++ b/includes/class-wcs-my-account-payment-methods.php
@@ -9,9 +9,6 @@
*/
class WCS_My_Account_Payment_Methods {
- /* A cache of a customer's payment tokens to avoid running multiple queries in the same request */
- protected static $customer_tokens = array();
-
/**
* Initialize filters and hooks for class.
*
@@ -27,6 +24,7 @@ class WCS_My_Account_Payment_Methods {
add_action( 'woocommerce_payment_token_deleted',array( __CLASS__, 'maybe_update_subscriptions_payment_meta' ), 10, 2 );
add_action( 'woocommerce_payment_token_set_default', array( __CLASS__, 'display_default_payment_token_change_notice' ), 10, 2 );
add_action( 'wp', array( __CLASS__, 'update_subscription_tokens' ) );
+
}
/**
@@ -41,8 +39,8 @@ class WCS_My_Account_Payment_Methods {
if ( $payment_token instanceof WC_Payment_Token && isset( $payment_token_data['actions']['delete']['url'] ) ) {
- if ( 0 < count( self::get_subscriptions_by_token( $payment_token ) ) ) {
- if ( self::customer_has_alternative_token( $payment_token ) ) {
+ if ( 0 < count( WCS_Payment_Tokens::get_subscriptions_from_token( $payment_token ) ) ) {
+ if ( WCS_Payment_Tokens::customer_has_alternative_token( $payment_token ) ) {
$delete_subscription_token_args = array(
'delete_subscription_token' => $payment_token->get_id(),
'wcs_nonce' => wp_create_nonce( 'delete_subscription_token_' . $payment_token->get_id() ),
@@ -76,7 +74,7 @@ class WCS_My_Account_Payment_Methods {
// init payment gateways
WC()->payment_gateways();
- $new_token = self::get_customers_alternative_token( $deleted_token );
+ $new_token = WCS_Payment_Tokens::get_customers_alternative_token( $deleted_token );
if ( empty( $new_token ) ) {
$notice = esc_html__( 'The deleted payment method was used for automatic subscription payments, we couldn\'t find an alternative token payment method token to change your subscriptions to.', 'woocommerce-subscriptions' );
@@ -84,20 +82,18 @@ class WCS_My_Account_Payment_Methods {
return;
}
- $subscriptions = self::get_subscriptions_by_token( $deleted_token );
+ $subscriptions = WCS_Payment_Tokens::get_subscriptions_from_token( $deleted_token );
if ( empty( $subscriptions ) ) {
return;
}
foreach ( $subscriptions as $subscription ) {
- $subscription = wcs_get_subscription( $subscription );
-
if ( empty( $subscription ) ) {
continue;
}
- if ( self::update_subscription_token( $subscription, $new_token, $deleted_token ) ) {
+ if ( WCS_Payment_Tokens::update_subscription_token( $subscription, $new_token, $deleted_token ) ) {
$subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer deleted a token from their My Account page. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $deleted_token->get_token(), $new_token->get_token() ) );
}
}
@@ -112,75 +108,6 @@ class WCS_My_Account_Payment_Methods {
wc_add_notice( $notice , 'notice' );
}
- /**
- * Update the subscription payment meta to change from an old payment token to a new one.
- *
- * @param WC_Subscription $subscription The subscription to update.
- * @param WC_Payment_Token $new_token The new payment token.
- * @param WC_Payment_Token $old_token The old payment token.
- * @return bool Whether the subscription was updated or not.
- */
- protected static function update_subscription_token( $subscription, $new_token, $old_token ) {
- $payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
- $token_payment_gateway = $old_token->get_gateway_id();
- $token_meta_key = '';
-
- // Attempt to find the token meta key from the subscription payment meta and the old token.
- if ( is_array( $payment_method_meta ) && isset( $payment_method_meta[ $token_payment_gateway ] ) && is_array( $payment_method_meta[ $token_payment_gateway ] ) ) {
- foreach ( $payment_method_meta[ $token_payment_gateway ] as $meta_table => $meta ) {
- foreach ( $meta as $meta_key => $meta_data ) {
- if ( $old_token->get_token() === $meta_data['value'] ) {
- $token_meta_key = $meta_key;
- break 2;
- }
- }
- }
- }
-
- $updated = update_post_meta( $subscription->get_id(), $token_meta_key, $new_token->get_token(), $old_token->get_token() );
-
- if ( $updated ) {
- do_action( 'woocommerce_subscription_token_changed', $subscription, $new_token, $old_token );
- }
-
- return $updated;
- }
-
- /**
- * Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method,
- * customer id and token value stored in post meta will be returned.
- *
- * @param WC_Payment_Token payment token object
- * @return array subscription posts
- * @since 2.2.7
- */
- public static function get_subscriptions_by_token( $payment_token ) {
-
- $meta_query = array(
- array(
- 'key' => '_payment_method',
- 'value' => $payment_token->get_gateway_id(),
- ),
- array(
- 'key' => '_requires_manual_renewal',
- 'value' => 'false',
- ),
- array(
- 'value' => $payment_token->get_token(),
- ),
- );
-
- $user_subscriptions = get_posts( array(
- 'post_type' => 'shop_subscription',
- 'post_status' => array( 'wc-pending', 'wc-active', 'wc-on-hold' ),
- 'meta_query' => $meta_query,
- 'posts_per_page' => -1,
- 'post__in' => WCS_Customer_Store::instance()->get_users_subscription_ids( $payment_token->get_user_id() ),
- ) );
-
- return apply_filters( 'woocommerce_subscriptions_by_payment_token', $user_subscriptions, $payment_token );
- }
-
/**
* Get a WC_Payment_Token label. eg Visa ending in 1234
*
@@ -199,66 +126,6 @@ class WCS_My_Account_Payment_Methods {
return $label;
}
- /**
- * Get a list of customer payment tokens. Caches results to avoid multiple database queries per request
- *
- * @param string (optional) Gateway ID for getting tokens for a specific gateway.
- * @param int (optional) The customer id - defaults to the current user.
- * @return array of WC_Payment_Token objects
- * @since 2.2.7
- */
- public static function get_customer_tokens( $gateway_id = '', $customer_id = '' ) {
- if ( '' === $customer_id ) {
- $customer_id = get_current_user_id();
- }
-
- if ( ! isset( self::$customer_tokens[ $customer_id ][ $gateway_id ] ) ) {
- self::$customer_tokens[ $customer_id ][ $gateway_id ] = WC_Payment_Tokens::get_customer_tokens( $customer_id, $gateway_id );
- }
-
- return self::$customer_tokens[ $customer_id ][ $gateway_id ];
- }
-
- /**
- * Get the customer's alternative token.
- *
- * @param WC_Payment_Token $token The token to find an alternative for
- * @return WC_Payment_Token The customer's alternative token
- * @since 2.2.7
- */
- public static function get_customers_alternative_token( $token ) {
- $payment_tokens = self::get_customer_tokens( $token->get_gateway_id(), $token->get_user_id() );
- $alternative_token = null;
-
- // Remove the token we're trying to find an alternative for.
- unset( $payment_tokens[ $token->get_id() ] );
-
- if ( count( $payment_tokens ) === 1 ) {
- $alternative_token = reset( $payment_tokens );
- } else {
- foreach ( $payment_tokens as $payment_token ) {
- // If there is a default token we can use it as an alternative.
- if ( $payment_token->is_default() ) {
- $alternative_token = $payment_token;
- break;
- }
- }
- }
-
- return $alternative_token;
- }
-
- /**
- * Determine if the customer has an alternative token.
- *
- * @param WC_Payment_Token payment token object
- * @return bool
- * @since 2.2.7
- */
- public static function customer_has_alternative_token( $token ) {
- return self::get_customers_alternative_token( $token ) !== null;
- }
-
/**
* Display a notice when a customer sets a new default token notifying them of what this means for their subscriptions.
*
@@ -268,12 +135,12 @@ class WCS_My_Account_Payment_Methods {
*/
public static function display_default_payment_token_change_notice( $default_token_id, $default_token ) {
$display_notice = false;
- $customer_tokens = self::get_customer_tokens( $default_token->get_gateway_id(), $default_token->get_user_id() );
+ $customer_tokens = WCS_Payment_Tokens::get_customer_tokens( $default_token->get_user_id(), $default_token->get_gateway_id() );
unset( $customer_tokens[ $default_token_id ] );
// Check if there are subscriptions for one of the customer's other tokens.
foreach ( $customer_tokens as $token ) {
- if ( count( self::get_subscriptions_by_token( $token ) ) > 0 ) {
+ if ( count( WCS_Payment_Tokens::get_subscriptions_from_token( $token ) ) > 0 ) {
$display_notice = true;
break;
}
@@ -317,14 +184,12 @@ class WCS_My_Account_Payment_Methods {
return;
}
- $tokens = self::get_customer_tokens( $default_token->get_gateway_id(), $default_token->get_user_id() );
+ $tokens = WCS_Payment_Tokens::get_customer_tokens( $default_token->get_user_id(), $default_token->get_gateway_id() );
unset( $tokens[ $default_token_id ] );
foreach ( $tokens as $old_token ) {
- foreach ( self::get_subscriptions_by_token( $old_token ) as $subscription ) {
- $subscription = wcs_get_subscription( $subscription );
-
- if ( ! empty( $subscription ) && self::update_subscription_token( $subscription, $default_token, $old_token ) ) {
+ foreach ( WCS_Payment_Tokens::get_subscriptions_from_token( $old_token ) as $subscription ) {
+ if ( ! empty( $subscription ) && WCS_Payment_Tokens::update_subscription_token( $subscription, $default_token, $old_token ) ) {
$subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer changed their default token and opted to update their subscriptions. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $old_token->get_token(), $default_token->get_token() ) );
}
}
@@ -333,4 +198,49 @@ class WCS_My_Account_Payment_Methods {
wp_redirect( remove_query_arg( array( 'update-subscription-tokens', 'token-id', '_wcsnonce' ) ) );
exit();
}
+
+ /**
+ * Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method,
+ * customer id and token value stored in post meta will be returned.
+ *
+ * @since 2.2.7
+ * @deprecated 2.5.0
+ */
+ public static function get_subscriptions_by_token( $payment_token ) {
+ _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_subscriptions_from_token()' );
+ return WCS_Payment_Tokens::get_subscriptions_from_token( $payment_token );
+ }
+
+ /**
+ * Get a list of customer payment tokens. Caches results to avoid multiple database queries per request
+ *
+ * @since 2.2.7
+ * @deprecated 2.5.0
+ */
+ public static function get_customer_tokens( $gateway_id = '', $customer_id = '' ) {
+ _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_customer_tokens()' );
+ return WCS_Payment_Tokens::get_customer_tokens( $customer_id, $gateway_id );
+ }
+
+ /**
+ * Get the customer's alternative token.
+ *
+ * @since 2.2.7
+ * @deprecated 2.5.0
+ */
+ public static function get_customers_alternative_token( $token ) {
+ _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_customers_alternative_token()' );
+ return WCS_Payment_Tokens::get_customers_alternative_token( $token );
+ }
+
+ /**
+ * Determine if the customer has an alternative token.
+ *
+ * @since 2.2.7
+ * @deprecated 2.5.0
+ */
+ public static function customer_has_alternative_token( $token ) {
+ _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::customer_has_alternative_token()' );
+ return WCS_Payment_Tokens::customer_has_alternative_token( $token );
+ }
}
diff --git a/includes/class-wcs-payment-tokens.php b/includes/class-wcs-payment-tokens.php
new file mode 100755
index 0000000..05c8401
--- /dev/null
+++ b/includes/class-wcs-payment-tokens.php
@@ -0,0 +1,180 @@
+get_gateway_id();
+ $payment_meta_table = self::get_subscription_payment_meta( $subscription, $token_payment_gateway );
+ $token_meta_key = '';
+
+ // Attempt to find the token meta key from the subscription payment meta and the old token.
+ if ( is_array( $payment_meta_table ) ) {
+ foreach ( $payment_meta_table as $meta_table => $meta ) {
+ foreach ( $meta as $meta_key => $meta_data ) {
+ if ( $old_token->get_token() === $meta_data['value'] ) {
+ $token_meta_key = $meta_key;
+ break 2;
+ }
+ }
+ }
+ }
+
+ $updated = update_post_meta( $subscription->get_id(), $token_meta_key, $new_token->get_token(), $old_token->get_token() );
+
+ if ( $updated ) {
+ do_action( 'woocommerce_subscription_token_changed', $subscription, $new_token, $old_token );
+ }
+
+ return $updated;
+ }
+
+ /**
+ * Get all payment meta on a subscription for a gateway.
+ *
+ * @param WC_Subscription $subscription The subscription to update.
+ * @param string $gateway_id The target gateway ID.
+ * @return bool|array Payment meta data. False if no meta is found.
+ * @since 2.5.0
+ */
+ public static function get_subscription_payment_meta( $subscription, $gateway_id ) {
+ $payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
+ if ( is_array( $payment_method_meta ) && isset( $payment_method_meta[ $gateway_id ] ) && is_array( $payment_method_meta[ $gateway_id ] ) ) {
+ return $payment_method_meta[ $gateway_id ];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method,
+ * customer id and token value stored in post meta will be returned.
+ *
+ * @param WC_Payment_Token $payment_token Payment token object.
+ * @return array subscription posts
+ * @since 2.5.0
+ */
+ public static function get_subscriptions_from_token( $payment_token ) {
+
+ $meta_query = array(
+ array(
+ 'key' => '_payment_method',
+ 'value' => $payment_token->get_gateway_id(),
+ ),
+ array(
+ 'key' => '_requires_manual_renewal',
+ 'value' => 'false',
+ ),
+ array(
+ 'value' => $payment_token->get_token(),
+ ),
+ );
+ $subscription_ids = get_posts( array(
+ 'post_type' => 'shop_subscription',
+ 'post_status' => array( 'wc-pending', 'wc-active', 'wc-on-hold' ),
+ 'meta_query' => $meta_query,
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ 'post__in' => WCS_Customer_Store::instance()->get_users_subscription_ids( $payment_token->get_user_id() ),
+ ) );
+
+ if ( has_filter( 'woocommerce_subscriptions_by_payment_token' ) ) {
+ wcs_deprecated_function( 'The "woocommerce_subscriptions_by_payment_token" hook should no longer be used. It previously filtered post objects and in moving to CRUD and Subscription APIs the "woocommerce_subscriptions_by_payment_token"', '2.5.0', 'woocommerce_subscriptions_from_payment_token' );
+
+ $subscription_posts = apply_filters( 'woocommerce_subscriptions_by_payment_token', array_map( 'get_post', $subscription_ids ), $payment_token );
+ $subscription_ids = array_unique( array_merge( $subscription_ids, wp_list_pluck( $subscription_posts, 'ID' ) ) );
+ }
+
+ $user_subscriptions = array();
+ foreach ( $subscription_ids as $subscription_id ) {
+ $user_subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id );
+ }
+
+ return apply_filters( 'woocommerce_subscriptions_from_payment_token', $user_subscriptions, $payment_token );
+ }
+
+ /**
+ * Get a list of customer payment tokens. Caches results to avoid multiple database queries per request
+ *
+ * @param int (optional) The customer id - defaults to the current user.
+ * @param string (optional) Gateway ID for getting tokens for a specific gateway.
+ * @return array of WC_Payment_Token objects.
+ * @since 2.2.7
+ */
+ public static function get_customer_tokens( $customer_id = '', $gateway_id = '' ) {
+ if ( '' === $customer_id ) {
+ $customer_id = get_current_user_id();
+ }
+
+ if ( ! isset( self::$customer_tokens[ $customer_id ][ $gateway_id ] ) ) {
+ self::$customer_tokens[ $customer_id ][ $gateway_id ] = parent::get_customer_tokens( $customer_id, $gateway_id );
+ }
+
+ return self::$customer_tokens[ $customer_id ][ $gateway_id ];
+ }
+
+ /**
+ * Get the customer's alternative token.
+ *
+ * @param WC_Payment_Token $token The token to find an alternative for.
+ * @return WC_Payment_Token The customer's alternative token.
+ * @since 2.2.7
+ */
+ public static function get_customers_alternative_token( $token ) {
+ $payment_tokens = self::get_customer_tokens( $token->get_gateway_id(), $token->get_user_id() );
+ $alternative_token = null;
+
+ // Remove the token we're trying to find an alternative for.
+ unset( $payment_tokens[ $token->get_id() ] );
+
+ if ( count( $payment_tokens ) === 1 ) {
+ $alternative_token = reset( $payment_tokens );
+ } else {
+ foreach ( $payment_tokens as $payment_token ) {
+ // If there is a default token we can use it as an alternative.
+ if ( $payment_token->is_default() ) {
+ $alternative_token = $payment_token;
+ break;
+ }
+ }
+ }
+
+ return $alternative_token;
+ }
+
+ /**
+ * Determine if the customer has an alternative token.
+ *
+ * @param WC_Payment_Token $token Payment token object.
+ * @return bool
+ * @since 2.2.7
+ */
+ public static function customer_has_alternative_token( $token ) {
+ return self::get_customers_alternative_token( $token ) !== null;
+ }
+
+}
diff --git a/includes/class-wcs-permalink-manager.php b/includes/class-wcs-permalink-manager.php
new file mode 100755
index 0000000..f80d07e
--- /dev/null
+++ b/includes/class-wcs-permalink-manager.php
@@ -0,0 +1,96 @@
+set_simple_content(
+ // translators: 1$-2$: opening and closing tags.
+ sprintf( esc_html__( 'Error saving Subscriptions endpoints: %1$sSubscriptions%2$s, %1$sView subscription%2$s and %1$sSubscription payment method%2$s cannot be the same. The changes have been reverted.', 'woocommerce-subscriptions' ), '', '' )
+ );
+ $notice->display();
+ }
+ }
+}
diff --git a/includes/class-wcs-query.php b/includes/class-wcs-query.php
index 1af0db7..7f49076 100755
--- a/includes/class-wcs-query.php
+++ b/includes/class-wcs-query.php
@@ -17,12 +17,18 @@ class WCS_Query extends WC_Query {
add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
add_action( 'parse_request', array( $this, 'parse_request' ), 0 );
add_filter( 'woocommerce_get_breadcrumb', array( $this, 'add_breadcrumb' ), 10 );
+ add_action( 'pre_get_posts', array( $this, 'maybe_redirect_payment_methods' ) );
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 11 );
add_filter( 'woocommerce_get_query_vars', array( $this, 'add_wcs_query_vars' ) );
// Inserting your new tab/page into the My Account page.
add_filter( 'woocommerce_account_menu_items', array( $this, 'add_menu_items' ) );
- add_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ), 10, 4 );
+
+ // Since WC 3.3.0, add_wcs_query_vars() is enough for custom endpoints to work.
+ if ( WC_Subscriptions::is_woocommerce_pre( '3.3.0' ) ) {
+ add_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ), 10, 4 );
+ }
+
add_filter( 'woocommerce_get_endpoint_url', array( $this, 'maybe_redirect_to_only_subscription' ), 10, 2 );
add_action( 'woocommerce_account_subscriptions_endpoint', array( $this, 'endpoint_content' ) );
}
@@ -47,6 +53,7 @@ class WCS_Query extends WC_Query {
);
if ( ! WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
$this->query_vars['subscriptions'] = get_option( 'woocommerce_myaccount_subscriptions_endpoint', 'subscriptions' );
+ $this->query_vars['subscription-payment-method'] = get_option( 'woocommerce_myaccount_subscription_payment_method_endpoint', 'subscription-payment-method' );
}
}
@@ -123,6 +130,12 @@ class WCS_Query extends WC_Query {
* @return array
*/
public function add_menu_items( $menu_items ) {
+
+ // If the Subscriptions endpoint setting is empty, don't display it in line with core WC behaviour.
+ if ( empty( $this->query_vars['subscriptions'] ) ) {
+ return $menu_items;
+ }
+
if ( 1 == count( wcs_get_users_subscriptions() ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) {
$label = __( 'My Subscription', 'woocommerce-subscriptions' );
} else {
@@ -148,7 +161,7 @@ class WCS_Query extends WC_Query {
* @return string
*/
public function maybe_redirect_to_only_subscription( $url, $endpoint ) {
- if ( 'subscriptions' == $endpoint && is_account_page() ) {
+ if ( $this->query_vars['subscriptions'] === $endpoint && is_account_page() ) {
$subscriptions = wcs_get_users_subscriptions();
if ( is_array( $subscriptions ) && 1 == count( $subscriptions ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) {
@@ -216,6 +229,41 @@ class WCS_Query extends WC_Query {
}
}
+ /**
+ * Redirect to order-pay flow for Subscription Payment Method endpoint.
+ *
+ * @param WP_Query $query WordPress query object
+ * @since 2.5.0
+ */
+ public function maybe_redirect_payment_methods( $query ) {
+
+ if ( ! $query->is_main_query() || ! absint( $query->get( 'subscription-payment-method' ) ) ) {
+ return;
+ }
+
+ $subscription = wcs_get_subscription( absint( $query->get( 'subscription-payment-method' ) ) );
+ if ( ! $subscription ) {
+ return;
+ }
+
+ if ( ! $subscription->can_be_updated_to( 'new-payment-method' ) ) {
+
+ $url = $subscription->get_view_order_url();
+ wc_add_notice( __( 'The payment method can not be changed for that subscription.', 'woocommerce-subscriptions' ), 'error' );
+
+ } else {
+
+ $args = array(
+ 'change_payment_method' => $subscription->get_id(),
+ '_wpnonce' => wp_create_nonce(),
+ );
+ $url = add_query_arg( $args, $subscription->get_checkout_payment_url() );
+ }
+
+ wp_redirect( $url );
+ exit();
+ }
+
/**
* Reset the woocommerce_myaccount_view_subscriptions_endpoint option name to woocommerce_myaccount_view_subscription_endpoint
*
@@ -262,8 +310,16 @@ class WCS_Query extends WC_Query {
'desc_tip' => true,
);
- WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_myaccount_view_order_endpoint', array( $subscriptions_endpoint_setting, $view_subscription_endpoint_setting ), 'multiple_settings' );
+ $subscription_payment_method_endpoint_setting = array(
+ 'title' => __( 'Subscription payment method', 'woocommerce-subscriptions' ),
+ 'desc' => __( 'Endpoint for the My Account → Change Subscription Payment Method page', 'woocommerce-subscriptions' ),
+ 'id' => 'woocommerce_myaccount_subscription_payment_method_endpoint',
+ 'type' => 'text',
+ 'default' => 'subscription-payment-method',
+ 'desc_tip' => true,
+ );
+ WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_myaccount_view_order_endpoint', array( $subscriptions_endpoint_setting, $view_subscription_endpoint_setting, $subscription_payment_method_endpoint_setting ), 'multiple_settings' );
return $settings;
}
@@ -279,7 +335,7 @@ class WCS_Query extends WC_Query {
* @return string $url
*/
- public function get_endpoint_url( $url, $endpoint, $value = '', $permalink = '') {
+ public function get_endpoint_url( $url, $endpoint, $value = '', $permalink = '' ) {
if ( ! empty( $this->query_vars[ $endpoint ] ) ) {
remove_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ) );
diff --git a/includes/class-wcs-retry-manager.php b/includes/class-wcs-retry-manager.php
index 4036ded..95d4bfc 100755
--- a/includes/class-wcs-retry-manager.php
+++ b/includes/class-wcs-retry-manager.php
@@ -63,7 +63,8 @@ class WCS_Retry_Manager {
add_action( 'woocommerce_subscriptions_retry_status_updated', __CLASS__ . '::maybe_delete_payment_retry_date', 0, 2 );
- add_action( 'woocommerce_subscription_renewal_payment_failed', __CLASS__ . '::maybe_apply_retry_rule', 10, 2 );
+ add_action( 'woocommerce_subscription_renewal_payment_failed', array( __CLASS__, 'maybe_apply_retry_rule' ), 10, 2 );
+ add_action( 'woocommerce_subscription_renewal_payment_failed', array( __CLASS__, 'maybe_reapply_last_retry_rule' ), 15, 2 );
add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' );
@@ -196,13 +197,13 @@ class WCS_Retry_Manager {
/**
* When a payment fails, apply a retry rule, if one exists that applies to this failure.
*
- * @param WC_Subscription The subscription on which the payment failed
- * @param WC_Order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param)
+ * @param WC_Subscription $subscription The subscription on which the payment failed.
+ * @param WC_Order $last_order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param).
+ *
* @since 2.1
*/
public static function maybe_apply_retry_rule( $subscription, $last_order ) {
-
- if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) ) {
+ if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) || ! self::is_scheduled_payment_attempt() ) {
return;
}
@@ -231,7 +232,7 @@ class WCS_Retry_Manager {
}
if ( $retry_rule->get_retry_interval() > 0 ) {
- // by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action
+ // by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action.
$subscription->update_dates( array( 'payment_retry' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval( $retry_count ) ) ) );
}
@@ -239,6 +240,36 @@ class WCS_Retry_Manager {
}
}
+ /**
+ * (Maybe) reapply last retry rule if:
+ * - Payment is no-scheduled
+ * - $last_order contains a Retry
+ * - Retry contains a rule
+ *
+ * @param WC_Subscription $subscription The subscription on which the payment failed.
+ * @param WC_Order $last_order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param).
+ *
+ * @since 2.5.0
+ */
+ public static function maybe_reapply_last_retry_rule( $subscription, $last_order ) {
+ // We're only interested in non-automatic payment attempts.
+ if ( self::is_scheduled_payment_attempt() ) {
+ return;
+ }
+
+ $last_retry = self::store()->get_last_retry_for_order( $last_order->get_id() );
+ if ( ! $last_retry || 'pending' !== $last_retry->get_status() || null === ( $last_retry_rule = $last_retry->get_rule() ) ) {
+ return;
+ }
+
+ foreach ( array( 'order' => $last_order, 'subscription' => $subscription ) as $object_type => $object ) {
+ $new_status = $last_retry_rule->get_status_to_apply( $object_type );
+ if ( '' !== $new_status && ! $object->has_status( $new_status ) ) {
+ $object->update_status( $new_status, _x( 'Retry rule reapplied:', 'used in order note as reason for why status changed', 'woocommerce-subscriptions' ) );
+ }
+ }
+ }
+
/**
* When a retry hook is triggered, check if the rules for that retry are still valid
* and if so, retry the payment.
@@ -391,9 +422,27 @@ class WCS_Retry_Manager {
}
}
+ /**
+ * Is `woocommerce_scheduled_subscription_payment` or `woocommerce_scheduled_subscription_payment_retry` current action?
+ *
+ * @return boolean
+ *
+ * @since 2.5.0
+ */
+ protected static function is_scheduled_payment_attempt() {
+ /**
+ * Filter 'Is scheduled payment attempt?'
+ *
+ * @param boolean doing_action( 'woocommerce_scheduled_subscription_payment' ) || doing_action( 'woocommerce_scheduled_subscription_payment_retry' )
+ * @since 2.5.0
+ */
+ return (bool) apply_filters( 'wcs_is_scheduled_payment_attempt', doing_action( 'woocommerce_scheduled_subscription_payment' ) || doing_action( 'woocommerce_scheduled_subscription_payment_retry' ) );
+ }
+
/**
* Access the object used to interface with the store.
*
+ * @return WCS_Retry_Store
* @since 2.4
*/
public static function store() {
diff --git a/includes/class-wcs-template-loader.php b/includes/class-wcs-template-loader.php
index b23ae35..4a2000d 100755
--- a/includes/class-wcs-template-loader.php
+++ b/includes/class-wcs-template-loader.php
@@ -8,39 +8,27 @@
class WCS_Template_Loader {
public static function init() {
- add_filter( 'wc_get_template', array( __CLASS__, 'add_view_subscription_template' ), 10, 5 );
add_action( 'woocommerce_account_view-subscription_endpoint', array( __CLASS__, 'get_view_subscription_template' ) );
add_action( 'woocommerce_subscription_details_table', array( __CLASS__, 'get_subscription_details_template' ) );
add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_subscription_totals_template' ) );
+ add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_order_downloads_template' ), 20 );
}
/**
- * Show the subscription template when view a subscription instead of loading the default order template.
- *
- * @param $located
- * @param $template_name
- * @param $args
- * @param $template_path
- * @param $default_path
- * @since 2.0
- */
- public static function add_view_subscription_template( $located, $template_name, $args, $template_path, $default_path ) {
- global $wp;
-
- if ( 'myaccount/my-account.php' == $template_name && ! empty( $wp->query_vars['view-subscription'] ) && WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
- $located = wc_locate_template( 'myaccount/view-subscription.php', $template_path, plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
- }
-
- return $located;
- }
-
- /**
- * Get the view subscription template. A post WC v2.6 compatible version of @see WCS_Template_Loader::add_view_subscription_template()
+ * Get the view subscription template.
*
+ * @param int $subscription_id Subscription ID.
* @since 2.0.17
*/
- public static function get_view_subscription_template() {
- wc_get_template( 'myaccount/view-subscription.php', array(), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
+ public static function get_view_subscription_template( $subscription_id ) {
+ $subscription = wcs_get_subscription( absint( $subscription_id ) );
+
+ if ( ! $subscription || ! current_user_can( 'view_order', $subscription->get_id() ) ) {
+ echo '
';
+ return;
+ }
+
+ wc_get_template( 'myaccount/view-subscription.php', compact( 'subscription' ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
}
/**
@@ -62,4 +50,16 @@ class WCS_Template_Loader {
public static function get_subscription_totals_template( $subscription ) {
wc_get_template( 'myaccount/subscription-totals.php', array( 'subscription' => $subscription ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
}
+
+ /**
+ * Get the order downloads template, which is part of the view subscription page.
+ *
+ * @param WC_Subscription $subscription Subscription object
+ * @since 2.5.0
+ */
+ public static function get_order_downloads_template( $subscription ) {
+ if ( $subscription->has_downloadable_item() && $subscription->is_download_permitted() ) {
+ wc_get_template( 'order/order-downloads.php', array( 'downloads' => $subscription->get_downloadable_items(), 'show_title' => true ) );
+ }
+ }
}
diff --git a/includes/data-stores/class-wcs-subscription-data-store-cpt.php b/includes/data-stores/class-wcs-subscription-data-store-cpt.php
index 21c8a27..d1f2a2f 100755
--- a/includes/data-stores/class-wcs-subscription-data-store-cpt.php
+++ b/includes/data-stores/class-wcs-subscription-data-store-cpt.php
@@ -155,20 +155,18 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements
}
}
- // On WC 3.5.0 the ID of the user that placed the order was moved from the post meta _customer_user to the post_author field in the wp_posts table.
- // If the update routine didn't manage to cover subscriptions, we need to use the value stored as post meta until our own update finishes.
- if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '>=' ) && 1 == $post_object->post_author && get_option( 'wcs_subscription_post_author_upgrade_is_scheduled', false ) ) {
- $props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true );
- } else {
- /**
- * WC 3.5.0 and our 2.4.0 post author upgrade scripts didn't account for subscriptions created manually by admin users with a user ID not equal to 1.
- * This resulted in those subscription post author columns not being updated and so linked to the admin user who created them, not the customer.
- *
- * Until a permanent fix is found, revert to the previous behavior of getting the customer user from post meta.
- * This will be eventually removed.
- *
- * @see https://github.com/Prospress/woocommerce-subscriptions/issues/3036
- */
+ /**
+ * WC 3.5.0 and our 2.4.0 post author upgrade scripts didn't account for subscriptions created manually by admin users with a user ID not equal to 1.
+ * This resulted in those subscription post author columns not being updated and so linked to the admin user who created them, not the customer.
+ *
+ * To make sure all subscriptions are linked to the correct customer, we revert to the previous behavior of
+ * getting the customer user from post meta.
+ * The fix is only applied on WC 3.5.0 because 3.5.1 brought back the old way (pre 3.5.0) of getting the
+ * customer ID for orders.
+ *
+ * @see https://github.com/Prospress/woocommerce-subscriptions/issues/3036
+ */
+ if ( '3.5.0' === WC()->version ) {
$props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true );
}
diff --git a/includes/early-renewal/class-wcs-cart-early-renewal.php b/includes/early-renewal/class-wcs-cart-early-renewal.php
index a8889d5..8ca8607 100755
--- a/includes/early-renewal/class-wcs-cart-early-renewal.php
+++ b/includes/early-renewal/class-wcs-cart-early-renewal.php
@@ -25,6 +25,7 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
// Check if a user is requesting to create an early renewal order for a subscription.
add_action( 'template_redirect', array( $this, 'maybe_setup_cart' ), 100 );
+ add_action( 'woocommerce_checkout_create_order', array( $this, 'copy_subscription_meta_to_order' ), 90 );
// Record early renewal payments.
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
add_action( 'woocommerce_checkout_order_processed', array( $this, 'maybe_record_early_renewal' ), 100, 2 );
@@ -149,6 +150,26 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
$subscription->update_status( 'on-hold', _x( 'Customer requested to renew early:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) );
}
+ /**
+ * Copies the metadata from the subscription to the order created on checkout.
+ *
+ * @param WC_Order $order The WC Order object.
+ *
+ * @since 2.5.2
+ */
+ public function copy_subscription_meta_to_order( $order ) {
+ $cart_item = $this->cart_contains();
+ if ( ! $cart_item ) {
+ return;
+ }
+
+ // Get the subscription.
+ $subscription = wcs_get_subscription( $cart_item[ $this->cart_item_key ]['subscription_id'] );
+
+ // Copy all meta from subscription to new renewal order
+ wcs_copy_order_meta( $subscription, $order, 'renewal_order' );
+ }
+
/**
* Adds the early renewal metadata to the order created on checkout.
*
diff --git a/includes/emails/class-wcs-email-cancelled-subscription.php b/includes/emails/class-wcs-email-cancelled-subscription.php
index 0a7b5c8..38eb1b2 100755
--- a/includes/emails/class-wcs-email-cancelled-subscription.php
+++ b/includes/emails/class-wcs-email-cancelled-subscription.php
@@ -45,6 +45,26 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
}
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
@@ -131,22 +151,22 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
// translators: placeholder is admin email
- 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ),
+ 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ),
'placeholder' => '',
'default' => '',
),
'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
- 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), $this->subject ),
- 'placeholder' => '',
+ 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '' . $this->subject . '' ),
+ 'placeholder' => $this->get_default_subject(),
'default' => '',
),
'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text',
- 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), $this->heading ),
- 'placeholder' => '',
+ 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), '' . $this->heading . '' ),
+ 'placeholder' => $this->get_default_heading(),
'default' => '',
),
'email_type' => array(
diff --git a/includes/emails/class-wcs-email-completed-renewal-order.php b/includes/emails/class-wcs-email-completed-renewal-order.php
index 9821d43..037aa85 100755
--- a/includes/emails/class-wcs-email-completed-renewal-order.php
+++ b/includes/emails/class-wcs-email-completed-renewal-order.php
@@ -46,6 +46,26 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
WC_Email::__construct();
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
diff --git a/includes/emails/class-wcs-email-completed-switch-order.php b/includes/emails/class-wcs-email-completed-switch-order.php
index d04d30c..b46cc32 100755
--- a/includes/emails/class-wcs-email-completed-switch-order.php
+++ b/includes/emails/class-wcs-email-completed-switch-order.php
@@ -45,6 +45,26 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
WC_Email::__construct();
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
diff --git a/includes/emails/class-wcs-email-customer-payment-retry.php b/includes/emails/class-wcs-email-customer-payment-retry.php
index bd416cb..a03e576 100755
--- a/includes/emails/class-wcs-email-customer-payment-retry.php
+++ b/includes/emails/class-wcs-email-customer-payment-retry.php
@@ -36,6 +36,28 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
WC_Email::__construct();
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @param bool $paid Whether the order has been paid or not.
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject( $paid = false ) {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @param bool $paid Whether the order has been paid or not.
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading( $paid = false ) {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
@@ -53,9 +75,9 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
$retry_time_index = array_search( '{retry_time}', $this->find );
if ( false === $retry_time_index ) {
$this->find['retry_time'] = '{retry_time}';
- $this->replace['retry_time'] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) );
+ $this->replace['retry_time'] = wcs_get_human_time_diff( $this->retry->get_time() );
} else {
- $this->replace[ $retry_time_index ] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) );
+ $this->replace[ $retry_time_index ] = wcs_get_human_time_diff( $this->retry->get_time() );
}
parent::trigger( $order_id, $order );
diff --git a/includes/emails/class-wcs-email-customer-renewal-invoice.php b/includes/emails/class-wcs-email-customer-renewal-invoice.php
index 82caa1c..4259a00 100755
--- a/includes/emails/class-wcs-email-customer-renewal-invoice.php
+++ b/includes/emails/class-wcs-email-customer-renewal-invoice.php
@@ -56,6 +56,28 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
WC_Email::__construct();
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @param bool $paid Whether the order has been paid or not.
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject( $paid = false ) {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @param bool $paid Whether the order has been paid or not.
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading( $paid = false ) {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
diff --git a/includes/emails/class-wcs-email-expired-subscription.php b/includes/emails/class-wcs-email-expired-subscription.php
index d1c80d3..01111b8 100755
--- a/includes/emails/class-wcs-email-expired-subscription.php
+++ b/includes/emails/class-wcs-email-expired-subscription.php
@@ -45,6 +45,26 @@ class WCS_Email_Expired_Subscription extends WC_Email {
}
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
@@ -129,22 +149,22 @@ class WCS_Email_Expired_Subscription extends WC_Email {
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
// translators: placeholder is admin email
- 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ),
+ 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '' . esc_html( get_option( 'admin_email' ) ) . '' ),
'placeholder' => '',
'default' => '',
),
'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
- 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), $this->subject ),
- 'placeholder' => '',
+ 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '' . $this->subject . '' ),
+ 'placeholder' => $this->get_default_subject(),
'default' => '',
),
'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), $this->heading ),
- 'placeholder' => '',
+ 'placeholder' => $this->get_default_heading(),
'default' => '',
),
'email_type' => array(
diff --git a/includes/emails/class-wcs-email-new-renewal-order.php b/includes/emails/class-wcs-email-new-renewal-order.php
index ecd3869..ffcedc3 100755
--- a/includes/emails/class-wcs-email-new-renewal-order.php
+++ b/includes/emails/class-wcs-email-new-renewal-order.php
@@ -48,6 +48,26 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
}
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
diff --git a/includes/emails/class-wcs-email-new-switch-order.php b/includes/emails/class-wcs-email-new-switch-order.php
index 6e8b31d..83ab856 100755
--- a/includes/emails/class-wcs-email-new-switch-order.php
+++ b/includes/emails/class-wcs-email-new-switch-order.php
@@ -43,6 +43,26 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
}
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
diff --git a/includes/emails/class-wcs-email-on-hold-subscription.php b/includes/emails/class-wcs-email-on-hold-subscription.php
index d42f9e1..e17b90c 100755
--- a/includes/emails/class-wcs-email-on-hold-subscription.php
+++ b/includes/emails/class-wcs-email-on-hold-subscription.php
@@ -45,6 +45,26 @@ class WCS_Email_On_Hold_Subscription extends WC_Email {
}
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
@@ -129,22 +149,22 @@ class WCS_Email_On_Hold_Subscription extends WC_Email {
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
// translators: placeholder is admin email
- 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ),
+ 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ),
'placeholder' => '',
'default' => '',
),
'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text',
- 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), $this->subject ),
- 'placeholder' => '',
+ 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '' . $this->subject . '' ),
+ 'placeholder' => $this->get_default_subject(),
'default' => '',
),
'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), $this->heading ),
- 'placeholder' => '',
+ 'placeholder' => $this->get_default_heading(),
'default' => '',
),
'email_type' => array(
diff --git a/includes/emails/class-wcs-email-payment-retry.php b/includes/emails/class-wcs-email-payment-retry.php
index d1cd514..a4d4f27 100755
--- a/includes/emails/class-wcs-email-payment-retry.php
+++ b/includes/emails/class-wcs-email-payment-retry.php
@@ -40,6 +40,26 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* Trigger.
*
@@ -53,7 +73,7 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
$this->find['retry-time'] = '{retry_time}';
$this->replace['order-date'] = wcs_format_datetime( wcs_get_objects_property( $this->object, 'date_created' ) );
$this->replace['order-number'] = $this->object->get_order_number();
- $this->replace['retry-time'] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) );
+ $this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() );
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
diff --git a/includes/emails/class-wcs-email-processing-renewal-order.php b/includes/emails/class-wcs-email-processing-renewal-order.php
index 70c6899..484a201 100755
--- a/includes/emails/class-wcs-email-processing-renewal-order.php
+++ b/includes/emails/class-wcs-email-processing-renewal-order.php
@@ -40,6 +40,26 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
WC_Email::__construct();
}
+ /**
+ * Get the default e-mail subject.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_subject() {
+ return $this->subject;
+ }
+
+ /**
+ * Get the default e-mail heading.
+ *
+ * @since 2.5.3
+ * @return string
+ */
+ public function get_default_heading() {
+ return $this->heading;
+ }
+
/**
* trigger function.
*
diff --git a/includes/gateways/class-wc-subscriptions-payment-gateways.php b/includes/gateways/class-wc-subscriptions-payment-gateways.php
index d364984..88ea101 100755
--- a/includes/gateways/class-wc-subscriptions-payment-gateways.php
+++ b/includes/gateways/class-wc-subscriptions-payment-gateways.php
@@ -21,16 +21,18 @@ class WC_Subscriptions_Payment_Gateways {
*/
public static function init() {
- add_action( 'init', __CLASS__ . '::init_paypal', 5 ); // run before default priority 10 in case the site is using ALTERNATE_WP_CRON to avoid https://core.trac.wordpress.org/ticket/24160
+ add_action( 'init', __CLASS__ . '::init_paypal', 5 ); // run before default priority 10 in case the site is using ALTERNATE_WP_CRON to avoid https://core.trac.wordpress.org/ticket/24160.
add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways' );
add_filter( 'woocommerce_no_available_payment_methods_message', __CLASS__ . '::no_available_payment_methods_message' );
- // Trigger a hook for gateways to charge recurring payments
+ add_filter( 'woocommerce_payment_gateways_renewal_support_status_html', __CLASS__ . '::payment_gateways_support_tooltip', 11, 2 );
+
+ // Trigger a hook for gateways to charge recurring payments.
add_action( 'woocommerce_scheduled_subscription_payment', __CLASS__ . '::gateway_scheduled_subscription_payment', 10, 1 );
- // Create a gateway specific hooks for subscription events
+ // Create a gateway specific hooks for subscription events.
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::trigger_gateway_status_updated_hook', 10, 2 );
}
@@ -214,6 +216,73 @@ class WC_Subscriptions_Payment_Gateways {
}
}
+ /**
+ * Display a list of each gateway supported features in a tooltip
+ *
+ * @since 2.5.0
+ */
+ public static function payment_gateways_support_tooltip( $status_html, $gateway ) {
+
+ if ( ( ! is_array( $gateway->supports ) || ! in_array( 'subscriptions', $gateway->supports ) ) && 'paypal' !== $gateway->id ) {
+ return $status_html;
+ }
+
+ $core_features = $gateway->supports;
+ $subscription_features = $change_payment_method_features = array();
+
+ foreach ( $core_features as $key => $feature ) {
+
+ // Skip any non-subscription related features.
+ if ( 0 !== strpos( $feature, 'subscription' ) ) {
+ continue;
+ }
+
+ $feature = str_replace( 'subscription_', '', $feature );
+
+ if ( 0 === strpos( $feature, 'payment_method' ) ) {
+ switch ( $feature ) {
+ case 'payment_method_change':
+ $change_payment_method_features[] = 'payment method change';
+ break;
+
+ case 'payment_method_change_customer':
+ $change_payment_method_features[] = 'customer change payment';
+ break;
+
+ case 'payment_method_change_admin':
+ $change_payment_method_features[] = 'admin change payment';
+ break;
+
+ default:
+ $change_payment_method_features[] = str_replace( 'payment_method', ' ', $feature );
+ break;
+ }
+ } else {
+ $subscription_features[] = $feature;
+ }
+
+ unset( $core_features[ $key ] );
+ }
+
+ $status_html .= '';
+
+ $allowed_html = wp_kses_allowed_html( 'post' );
+ $allowed_html['span']['data-tip'] = true;
+
+ return wp_kses( $status_html, $allowed_html );
+ }
+
/**
* Fire a gateway specific hook for when a subscription is activated.
*
diff --git a/includes/gateways/paypal/class-wcs-paypal.php b/includes/gateways/paypal/class-wcs-paypal.php
index 250b1da..0c42dbf 100755
--- a/includes/gateways/paypal/class-wcs-paypal.php
+++ b/includes/gateways/paypal/class-wcs-paypal.php
@@ -34,7 +34,7 @@ class WCS_PayPal {
* Main PayPal Instance, ensures only one instance is/can be loaded
*
* @see wc_paypal_express()
- * @return WC_PayPal_Express
+ * @return WCS_PayPal
* @since 2.0
*/
public static function instance() {
@@ -80,18 +80,32 @@ class WCS_PayPal {
add_filter( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', __CLASS__ . '::maybe_add_change_payment_method_warning' );
+ // Maybe order don't need payment because lock.
+ add_filter( 'woocommerce_order_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 2 );
+
+ // Remove payment lock when order is completely paid or order is cancelled.
+ add_action( 'woocommerce_order_status_cancelled', __CLASS__ . '::maybe_remove_payment_lock' );
+ add_action( 'woocommerce_payment_complete', __CLASS__ . '::maybe_remove_payment_lock' );
+
+ // Adds payment lock on order received.
+ add_action( 'get_header', __CLASS__ . '::maybe_add_payment_lock' );
+
// Run the IPN failure handler attach and detach functions before and after processing to catch and log any unexpected shutdowns
add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::attach', -1, 1 );
add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::detach', 1, 1 );
+ // Remove PayPal from the available payment methods if it's disabled for subscription purchases.
+ add_filter( 'woocommerce_available_payment_gateways', array( __CLASS__, 'maybe_remove_paypal_standard' ) );
+
WCS_PayPal_Supports::init();
WCS_PayPal_Status_Manager::init();
WCS_PayPal_Standard_Switcher::init();
if ( is_admin() ) {
WCS_PayPal_Admin::init();
- WCS_PayPal_Change_Payment_Method_Admin::init();
}
+
+ WCS_PayPal_Change_Payment_Method_Admin::init();
}
/**
@@ -227,6 +241,11 @@ class WCS_PayPal {
// Store the billing agreement ID on the order and subscriptions
wcs_set_paypal_id( $order, $billing_agreement_response->get_billing_agreement_id() );
+ // Update payment method on all active subscriptions?
+ if ( wcs_is_subscription( $order ) && WC_Subscriptions_Change_Payment_Gateway::will_subscription_update_all_payment_methods( $order ) ) {
+ WC_Subscriptions_Change_Payment_Gateway::update_all_payment_methods_from_subscription( $order, $payment_method->id );
+ }
+
foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) ) as $subscription ) {
$subscription->set_payment_method( $payment_method );
wcs_set_paypal_id( $subscription, $billing_agreement_response->get_billing_agreement_id() ); // Also saves the subscription
@@ -438,6 +457,74 @@ class WCS_PayPal {
return $script_parameters;
}
+ /**
+ * This validates against payment lock for PP and returns false if we meet the criteria:
+ * - is a parent order.
+ * - payment method is paypal.
+ * - PayPal Reference Transactions is disabled.
+ * - order has lock.
+ * - lock hasn't timeout.
+ *
+ * @param bool $needs_payment Does this order needs to process payment?
+ * @param WC_Order $order The actual order.
+ *
+ * @return bool
+ * @since 2.5.3
+ */
+ public static function maybe_override_needs_payment( $needs_payment, $order ) {
+ if ( $needs_payment && self::instance()->get_id() === $order->get_payment_method() && ! self::are_reference_transactions_enabled() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) {
+ $has_lock = $order->get_meta( '_wcs_lock_order_payment' );
+ $seconds_since_order = wcs_seconds_since_order_created( $order );
+
+ // We have lock and order hasn't meet the lock time.
+ if ( $has_lock && $seconds_since_order < apply_filters( 'wcs_lock_order_payment_seconds', 180 ) ) {
+ $needs_payment = false;
+ }
+ }
+
+ return $needs_payment;
+ }
+
+ /**
+ * Adds payment lock meta when order is received and...
+ * - order is valid.
+ * - payment method is paypal.
+ * - order needs payment.
+ * - PayPal Reference Transactions is disabled.
+ * - order is parent order of a subscription.
+ *
+ * @since 2.5.3
+ */
+ public static function maybe_add_payment_lock() {
+ if ( ! wcs_is_order_received_page() ) {
+ return;
+ }
+
+ global $wp;
+ $order = wc_get_order( absint( $wp->query_vars['order-received'] ) );
+
+ if ( $order && self::instance()->get_id() === $order->get_payment_method() && $order->needs_payment() && ! self::are_reference_transactions_enabled() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) {
+ $order->update_meta_data( '_wcs_lock_order_payment', 'true' );
+ $order->save();
+ }
+ }
+
+ /**
+ * Removes payment lock when order is parent and has paypal method.
+ *
+ * @param int $order_id Order cancelled/paid.
+ *
+ * @since 2.5.3
+ */
+ public static function maybe_remove_payment_lock( $order_id ) {
+ $order = wc_get_order( $order_id );
+
+ if ( self::instance()->get_id() === $order->get_payment_method() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) {
+ $order->delete_meta_data( 'wcs_lock_order_payment' );
+ $order->save();
+ }
+ }
+
/** Getters ******************************************************/
/**
@@ -540,4 +627,65 @@ class WCS_PayPal {
return 'paypal';
}
+ /**
+ * Set the default value for whether PayPal Standard is enabled or disabled for subscriptions purchases.
+ *
+ * PayPal Standard will be enabled for subscriptions when:
+ * - PayPal is enabled.
+ * - The store has existing subscriptions.
+ *
+ * In any other case, it will be disabled by default.
+ * This function is called when 2.5.0 is active for the first time. @see WC_Subscriptions_Upgrader::upgrade()
+ *
+ * @since 2.5.0
+ */
+ public static function set_enabled_for_subscriptions_default() {
+
+ // Exit early if it has already been set.
+ if ( self::get_option( 'enabled_for_subscriptions' ) ) {
+ return;
+ }
+
+ // For existing stores with PayPal enabled, PayPal is automatically enabled for subscriptions.
+ if ( 'yes' === WCS_PayPal::get_option( 'enabled' ) && wcs_do_subscriptions_exist() ) {
+ $default = 'yes';
+ } else {
+ $default = 'no';
+ }
+
+ // Find the PayPal Standard gateway instance to set the setting.
+ foreach ( WC()->payment_gateways->payment_gateways as $gateway ) {
+ if ( $gateway->id === 'paypal' ) {
+ wcs_update_settings_option( $gateway, 'enabled_for_subscriptions', $default );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Remove PayPal Standard as an available payment method if it is disabled for subscriptions.
+ *
+ * @param array $available_gateways A list of available payment methods displayed on the checkout.
+ * @return array
+ * @since 2.5.0
+ */
+ public static function maybe_remove_paypal_standard( $available_gateways ) {
+
+ if ( ! isset( $available_gateways['paypal'] ) || 'yes' === self::get_option( 'enabled_for_subscriptions' ) || WCS_PayPal::are_reference_transactions_enabled() ) {
+ return $available_gateways;
+ }
+
+ $paying_for_order = absint( get_query_var( 'order-pay' ) );
+
+ if (
+ WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment ||
+ WC_Subscriptions_Cart::cart_contains_subscription() ||
+ wcs_cart_contains_renewal() ||
+ ( $paying_for_order && wcs_order_contains_subscription( $paying_for_order ) )
+ ) {
+ unset( $available_gateways['paypal'] );
+ }
+
+ return $available_gateways;
+ }
}
diff --git a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php
index b6f4b5c..1376f30 100755
--- a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php
+++ b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php
@@ -39,6 +39,9 @@ class WCS_PayPal_Admin {
// Before WC updates the PayPal settings remove credentials error flag
add_action( 'load-woocommerce_page_wc-settings', __CLASS__ . '::maybe_update_credentials_error_flag', 9 );
+
+ // Add an enable for subscription purchases setting.
+ add_action( 'woocommerce_settings_api_form_fields_paypal', array( __CLASS__, 'add_enable_for_subscriptions_setting' ) );
}
/**
@@ -56,7 +59,8 @@ class WCS_PayPal_Admin {
// Warn store managers not to change their PayPal Email address as it can break existing Subscriptions in WC2.0+
WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['desc_tip'] = false;
- WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['description'] .= '
' . __( 'It is strongly recommended you do not change the Receiver Email address if you have active subscriptions with PayPal. Doing so can break existing subscriptions.', 'woocommerce-subscriptions' );
+ // translators: $1 and $2 are opening and closing strong tags, respectively.
+ WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['description'] .= '
' . sprintf( __( 'It is %sstrongly recommended you do not change the Receiver Email address%s if you have active subscriptions with PayPal. Doing so can break existing subscriptions.', 'woocommerce-subscriptions' ), '', '' );
}
}
@@ -244,7 +248,7 @@ class WCS_PayPal_Admin {
* @param WC_Subscription $subscription
*/
public static function profile_link( $subscription ) {
- if ( wcs_is_subscription( $subscription ) && 'paypal' == $subscription->get_payment_method() ) {
+ if ( wcs_is_subscription( $subscription ) && ! $subscription->is_manual() && 'paypal' == $subscription->get_payment_method() ) {
$paypal_profile_id = wcs_get_paypal_id( $subscription );
@@ -275,4 +279,35 @@ class WCS_PayPal_Admin {
}
+ /**
+ * Add the enabled or subscriptions setting.
+ *
+ * @param array $settings The WooCommerce PayPal Settings array.
+ * @return array
+ * @since 2.5.0
+ */
+ public static function add_enable_for_subscriptions_setting( $settings ) {
+ if ( WCS_PayPal::are_reference_transactions_enabled() ) {
+ return $settings;
+ }
+
+ $setting = array(
+ 'type' => 'checkbox',
+ 'label' => __( 'Enable PayPal Standard for Subscriptions', 'woocommerce-subscriptions' ),
+ 'default' => 'no',
+ );
+
+ // Display a description
+ if ( 'no' === WCS_PayPal::get_option( 'enabled_for_subscriptions' ) ) {
+ $setting['description'] = sprintf(
+ /* translators: Placeholders are the opening and closing link tags.*/
+ __( "Before enabling PayPal Standard for Subscriptions, please note, when using PayPal Standard, customers are locked into using PayPal Standard for the life of their subscription, and PayPal Standard has a number of limitations. Please read the guide on %swhy we don't recommend PayPal Standard%s for Subscriptions before choosing to enable this option.", 'woocommerce-subscriptions' ),
+ '', ''
+ );
+ }
+
+ $settings = wcs_array_insert_after( 'enabled', $settings, 'enabled_for_subscriptions', $setting );
+
+ return $settings;
+ }
}
diff --git a/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php b/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php
index bdc150c..f9348b6 100755
--- a/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php
+++ b/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php
@@ -28,7 +28,9 @@ class WCS_PayPal_Change_Payment_Method_Admin {
add_filter( 'woocommerce_subscription_payment_meta', __CLASS__ . '::add_payment_meta_details', 10, 2 );
// Validate the PayPal billing agreement ID meta value when attempting to set PayPal as the payment method
- add_filter( 'woocommerce_subscription_validate_payment_meta_paypal', __CLASS__ . '::validate_payment_meta', 10, 2 );
+ if ( is_admin() ) {
+ add_filter( 'woocommerce_subscription_validate_payment_meta_paypal', __CLASS__ . '::validate_payment_meta', 10, 2 );
+ }
}
/**
diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php
index 757a276..0815489 100755
--- a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php
+++ b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php
@@ -252,7 +252,7 @@ class WCS_PayPal_Reference_Transaction_API_Request {
'ITEMURL' => $product->get_permalink(),
);
- $order_subtotal += $item['line_total'];
+ $order_subtotal += $order->get_line_total( $item );
}
// add fees
@@ -264,7 +264,7 @@ class WCS_PayPal_Reference_Transaction_API_Request {
'QTY' => 1,
);
- $order_subtotal += $fee['line_total'];
+ $order_subtotal += $order->get_line_total( $fee );
}
// add discounts
@@ -550,18 +550,17 @@ class WCS_PayPal_Reference_Transaction_API_Request {
/**
* Returns the request parameters after validation & filtering
*
- * @throws \SV_WC_Payment_Gateway_Exception invalid amount
+ * @throws \Exception invalid amount
* @return array request parameters
* @since 2.0
*/
public function get_parameters() {
-
/**
* Filter PPE request parameters.
*
* Use this to modify the PayPal request parameters prior to validation
*
- * @param array $parameters
+ * @param array $parameters
* @param \WC_PayPal_Express_API_Request $this instance
*/
$this->parameters = apply_filters( 'wcs_paypal_request_params', $this->parameters, $this );
@@ -579,8 +578,7 @@ class WCS_PayPal_Reference_Transaction_API_Request {
// amounts must be 10,000.00 or less for USD
if ( isset( $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] ) && 'USD' == $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] && $value > 10000 ) {
-
- throw new SV_WC_Payment_Gateway_Exception( sprintf( '%s amount of %s must be less than $10,000.00', $key, $value ) );
+ throw new Exception( sprintf( '%s amount of %s must be less than $10,000.00', $key, wc_price( $value ) ) );
}
// PayPal requires locale-specific number formats (e.g. USD is 123.45)
diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php
index 496e3ff..58b1450 100755
--- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php
+++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php
@@ -248,15 +248,16 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
switch ( $transaction_details['txn_type'] ) {
case 'subscr_signup':
+ $order = self::get_parent_order_with_fallback( $subscription );
// Store PayPal Details on Subscription and Order
$this->save_paypal_meta_data( $subscription, $transaction_details );
- $this->save_paypal_meta_data( $subscription->get_parent(), $transaction_details );
+ $this->save_paypal_meta_data( $order, $transaction_details );
// When there is a free trial & no initial payment amount, we need to mark the order as paid and activate the subscription
- if ( ! $is_payment_change && ! $is_renewal_sign_up_after_failure && 0 == $subscription->get_parent()->get_total() ) {
+ if ( ! $is_payment_change && ! $is_renewal_sign_up_after_failure && 0 == $order->get_total() ) {
// Safe to assume the subscription has an order here because otherwise we wouldn't get a 'subscr_signup' IPN
- $subscription->get_parent()->payment_complete(); // No 'txn_id' value for 'subscr_signup' IPN messages
+ $order->payment_complete(); // No 'txn_id' value for 'subscr_signup' IPN messages
update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' );
}
@@ -288,8 +289,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
break;
case 'subscr_payment':
-
- if ( 0.01 == $transaction_details['mc_gross'] && 1 == $subscription->get_completed_payment_count() ) {
+ if ( 0.01 == $transaction_details['mc_gross'] ) {
WC_Gateway_Paypal::log( 'IPN ignored, treating IPN as secondary trial period.' );
exit;
}
@@ -335,17 +335,18 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
// First payment on order, process payment & activate subscription
if ( $is_first_payment ) {
- $parent_order = $subscription->get_parent();
+ $parent_order = self::get_parent_order_with_fallback( $subscription );
if ( ! $parent_order->is_paid() ) {
$parent_order->payment_complete( $transaction_details['txn_id'] );
- } elseif ( $subscription->can_be_updated_to( 'active' ) ) {
- // If the order has already been paid it might have been completed via PDT so reactivate the subscription now because calling payment complete won't.
+ }
+
+ if ( $subscription->can_be_updated_to( 'active' ) ) {
$subscription->update_status( 'active' );
}
// Store PayPal Details on Order
- $this->save_paypal_meta_data( $subscription->get_parent(), $transaction_details );
+ $this->save_paypal_meta_data( $parent_order, $transaction_details );
// IPN got here first or PDT will never arrive. Normally PDT would have arrived, so the first IPN would not be the first payment. In case the the first payment is an IPN, we need to make sure to not ignore the second one
update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' );
@@ -457,7 +458,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
// Make sure subscription hasn't been linked to a new payment method
if ( wcs_get_paypal_id( $subscription ) != $ipn_profile_id ) {
- WC_Gateway_Paypal::log( sprintf( 'IPN "recurring_payment_suspended" ignored for subscription %d - PayPal profile ID has changed', $subscription->id ) );
+ WC_Gateway_Paypal::log( sprintf( 'IPN "recurring_payment_suspended" ignored for subscription %d - PayPal profile ID has changed', $subscription->get_id() ) );
} else if ( $subscription->has_status( 'active' ) ) {
@@ -648,6 +649,22 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
return array( 'order_id' => (int) $order_id, 'order_key' => $order_key );
}
+ /**
+ * This function will try to get the parent order, and if not available, will get the last order related to the Subscription.
+ *
+ * @param WC_Subscription $subscription The Subscription.
+ *
+ * @return WC_Order Parent order or the last related order (renewal)
+ */
+ protected static function get_parent_order_with_fallback( $subscription ) {
+ $order = $subscription->get_parent();
+ if ( ! $order ) {
+ $order = $subscription->get_last_order( 'all' );
+ }
+
+ return $order;
+ }
+
/**
* Cancel a specific PayPal Standard Subscription Profile with PayPal.
*
diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-supports.php b/includes/gateways/paypal/includes/class-wcs-paypal-supports.php
index 3a88e1d..4e92c16 100755
--- a/includes/gateways/paypal/includes/class-wcs-paypal-supports.php
+++ b/includes/gateways/paypal/includes/class-wcs-paypal-supports.php
@@ -35,6 +35,7 @@ class WCS_PayPal_Supports {
'subscription_amount_changes',
'subscription_date_changes',
'multiple_subscriptions',
+ 'subscription_payment_method_delayed_change',
);
/**
diff --git a/includes/libraries/action-scheduler/action-scheduler.php b/includes/libraries/action-scheduler/action-scheduler.php
index 28b37f1..ce8fee4 100755
--- a/includes/libraries/action-scheduler/action-scheduler.php
+++ b/includes/libraries/action-scheduler/action-scheduler.php
@@ -5,7 +5,7 @@
* Description: A robust scheduling library for use in WordPress plugins.
* Author: Prospress
* Author URI: http://prospress.com/
- * Version: 2.1.1
+ * Version: 2.2.1
* License: GPLv3
*
* Copyright 2018 Prospress, Inc. (email : freedoms@prospress.com)
@@ -25,21 +25,21 @@
*
*/
-if ( ! function_exists( 'action_scheduler_register_2_dot_1_dot_1' ) ) {
+if ( ! function_exists( 'action_scheduler_register_2_dot_2_dot_1' ) ) {
if ( ! class_exists( 'ActionScheduler_Versions' ) ) {
require_once( 'classes/ActionScheduler_Versions.php' );
add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 );
}
- add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_1_dot_1', 0, 0 );
+ add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_2_dot_1', 0, 0 );
- function action_scheduler_register_2_dot_1_dot_1() {
+ function action_scheduler_register_2_dot_2_dot_1() {
$versions = ActionScheduler_Versions::instance();
- $versions->register( '2.1.1', 'action_scheduler_initialize_2_dot_1_dot_1' );
+ $versions->register( '2.2.1', 'action_scheduler_initialize_2_dot_2_dot_1' );
}
- function action_scheduler_initialize_2_dot_1_dot_1() {
+ function action_scheduler_initialize_2_dot_2_dot_1() {
require_once( 'classes/ActionScheduler.php' );
ActionScheduler::init( __FILE__ );
}
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler.php b/includes/libraries/action-scheduler/classes/ActionScheduler.php
index 51af2b5..2c5d031 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler.php
@@ -85,6 +85,11 @@ abstract class ActionScheduler {
self::$plugin_file = $plugin_file;
spl_autoload_register( array( __CLASS__, 'autoload' ) );
+ /**
+ * Fires in the early stages of Action Scheduler init hook.
+ */
+ do_action( 'action_scheduler_pre_init' );
+
$store = self::store();
add_action( 'init', array( $store, 'init' ), 1, 0 );
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php
index 5259797..e34fb87 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php
@@ -57,13 +57,16 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
$action = $this->store->fetch_action( $action_id );
$this->store->log_execution( $action_id );
$action->execute();
- do_action( 'action_scheduler_after_execute', $action_id );
+ do_action( 'action_scheduler_after_execute', $action_id, $action );
$this->store->mark_complete( $action_id );
} catch ( Exception $e ) {
$this->store->mark_failure( $action_id );
do_action( 'action_scheduler_failed_execution', $action_id, $e );
}
- $this->schedule_next_instance( $action );
+
+ if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) ) {
+ $this->schedule_next_instance( $action );
+ }
}
/**
@@ -86,7 +89,7 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
* @author Jeremy Pry
*/
protected function run_cleanup() {
- $this->cleaner->clean();
+ $this->cleaner->clean( 10 * $this->get_time_limit() );
}
/**
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php b/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php
index b1abf12..91d8b18 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php
@@ -30,6 +30,7 @@ class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated {
if ( class_exists( 'WooCommerce' ) ) {
add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) );
+ add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) );
add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) );
}
@@ -37,6 +38,10 @@ class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated {
}
}
+ public function system_status_report() {
+ $table = new ActionScheduler_wcSystemStatus( ActionScheduler::store() );
+ $table->print();
+ }
/**
* Registers action-scheduler into WooCommerce > System status.
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php
index 3e7db8b..0441e40 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php
@@ -31,6 +31,13 @@ class ActionScheduler_CronSchedule implements ActionScheduler_Schedule {
return true;
}
+ /**
+ * @return string
+ */
+ public function get_recurrence() {
+ return strval($this->cron);
+ }
+
/**
* For PHP 5.2 compat, since DateTime objects can't be serialized
* @return array
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php
index 76068e1..604ad61 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php
@@ -36,9 +36,7 @@ class ActionScheduler_IntervalSchedule implements ActionScheduler_Schedule {
}
/**
- * @param DateTime $after
- *
- * @return DateTime|null
+ * @return int
*/
public function interval_in_seconds() {
return $this->interval_in_seconds;
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php b/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php
index ad26de8..95f97d6 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php
@@ -222,9 +222,16 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
*/
protected function get_recurrence( $action ) {
$recurrence = $action->get_schedule();
- if ( method_exists( $recurrence, 'interval_in_seconds' ) ) {
- return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence->interval_in_seconds() ) );
+ if ( $recurrence->is_recurring() ) {
+ if ( method_exists( $recurrence, 'interval_in_seconds' ) ) {
+ return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence->interval_in_seconds() ) );
+ }
+
+ if ( method_exists( $recurrence, 'get_recurrence' ) ) {
+ return sprintf( __( 'Cron %s', 'action-scheduler' ), $recurrence->get_recurrence() );
+ }
}
+
return __( 'Non-repeating', 'action-scheduler' );
}
@@ -280,7 +287,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
$date = $log_entry->get_date();
$date->setTimezone( $timezone );
- return sprintf( '
', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) );
}
/**
@@ -378,7 +385,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
$next_timestamp = $schedule->next()->getTimestamp();
- $schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s e' );
+ $schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s O' );
$schedule_display_string .= ' ';
if ( gmdate( 'U' ) > $next_timestamp ) {
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php
index 30933ce..3da06ab 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php
@@ -73,7 +73,7 @@ abstract class ActionScheduler_Logger {
$this->log( $action_id, __( 'action complete', 'action-scheduler' ) );
}
- public function log_failed_action( $action_id, \Exception $exception ) {
+ public function log_failed_action( $action_id, Exception $exception ) {
$this->log( $action_id, sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() ) );
}
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php
index 5daf11b..1da13ab 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php
@@ -18,13 +18,6 @@ class ActionScheduler_QueueCleaner {
*/
private $month_in_seconds = 2678400;
- /**
- * Five minutes in seconds
- *
- * @var int
- */
- private $five_minutes = 300;
-
/**
* ActionScheduler_QueueCleaner constructor.
*
@@ -77,8 +70,16 @@ class ActionScheduler_QueueCleaner {
}
}
- public function reset_timeouts() {
- $timeout = apply_filters( 'action_scheduler_timeout_period', $this->five_minutes );
+ /**
+ * Unclaim pending actions that have not been run within a given time limit.
+ *
+ * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
+ * as a parameter is 10x the time limit used for queue processing.
+ *
+ * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes).
+ */
+ public function reset_timeouts( $time_limit = 300 ) {
+ $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit );
if ( $timeout < 0 ) {
return;
}
@@ -97,8 +98,17 @@ class ActionScheduler_QueueCleaner {
}
}
- public function mark_failures() {
- $timeout = apply_filters( 'action_scheduler_failure_period', $this->five_minutes );
+ /**
+ * Mark actions that have been running for more than a given time limit as failed, based on
+ * the assumption some uncatachable and unloggable fatal error occurred during processing.
+ *
+ * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
+ * as a parameter is 10x the time limit used for queue processing.
+ *
+ * @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes).
+ */
+ public function mark_failures( $time_limit = 300 ) {
+ $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit );
if ( $timeout < 0 ) {
return;
}
@@ -119,12 +129,13 @@ class ActionScheduler_QueueCleaner {
/**
* Do all of the cleaning actions.
*
+ * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes).
* @author Jeremy Pry
*/
- public function clean() {
+ public function clean( $time_limit = 300 ) {
$this->delete_old_actions();
- $this->reset_timeouts();
- $this->mark_failures();
+ $this->reset_timeouts( $time_limit );
+ $this->mark_failures( $time_limit );
}
/**
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php
index b3f1730..f58b718 100755
--- a/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php
@@ -27,7 +27,8 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu
*/
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) {
if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
- throw new Exception( __( 'The ' . __CLASS__ . ' class can only be run within WP CLI.', 'action-scheduler' ) );
+ /* translators: %s php class name */
+ throw new Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), __CLASS__ ) );
}
parent::__construct( $store, $monitor, $cleaner );
@@ -76,7 +77,7 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu
*/
protected function add_hooks() {
add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) );
- add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ) );
+ add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 );
add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 );
}
@@ -143,11 +144,16 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu
*
* @author Jeremy Pry
*
- * @param $action_id
+ * @param int $action_id
+ * @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility.
*/
- public function after_execute( $action_id ) {
+ public function after_execute( $action_id, $action = null ) {
+ // backward compatibility
+ if ( null === $action ) {
+ $action = $this->store->fetch_action( $action_id );
+ }
/* translators: %s refers to the action ID */
- WP_CLI::log( sprintf( __( 'Completed processing action %s', 'action-scheduler' ), $action_id ) );
+ WP_CLI::log( sprintf( __( 'Completed processing action %s with hook: %s', 'action-scheduler' ), $action_id, $action->get_hook() ) );
}
/**
diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php
new file mode 100755
index 0000000..ec7f5a4
--- /dev/null
+++ b/includes/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php
@@ -0,0 +1,129 @@
+store = $store;
+ }
+
+ /**
+ * Display action data, including number of actions grouped by status and the oldest & newest action in each status.
+ *
+ * Helpful to identify issues, like a clogged queue.
+ */
+ public function print() {
+ $action_counts = $this->store->action_counts();
+ $status_labels = $this->store->get_status_labels();
+ $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) );
+
+ $this->get_template( $status_labels, $action_counts, $oldest_and_newest );
+ }
+
+ /**
+ * Get oldest and newest scheduled dates for a given set of statuses.
+ *
+ * @param array $status_keys Set of statuses to find oldest & newest action for.
+ * @return array
+ */
+ protected function get_oldest_and_newest( $status_keys ) {
+
+ $oldest_and_newest = array();
+
+ foreach ( $status_keys as $status ) {
+ $oldest_and_newest[ $status ] = array(
+ 'oldest' => '–',
+ 'newest' => '–',
+ );
+
+ if ( 'in-progress' === $status ) {
+ continue;
+ }
+
+ $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' );
+ $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' );
+ }
+
+ return $oldest_and_newest;
+ }
+
+ /**
+ * Get oldest or newest scheduled date for a given status.
+ *
+ * @param string $status Action status label/name string.
+ * @param string $date_type Oldest or Newest.
+ * @return DateTime
+ */
+ protected function get_action_status_date( $status, $date_type = 'oldest' ) {
+
+ $order = 'oldest' === $date_type ? 'ASC' : 'DESC';
+
+ $action = $this->store->query_actions( array(
+ 'claimed' => false,
+ 'status' => $status,
+ 'per_page' => 1,
+ 'order' => $order,
+ ) );
+
+ if ( ! empty( $action ) ) {
+ $date_object = $this->store->get_date( $action[0] );
+ $action_date = $date_object->format( 'Y-m-d H:i:s O' );
+ } else {
+ $action_date = '–';
+ }
+
+ return $action_date;
+ }
+
+ /**
+ * Get oldest or newest scheduled date for a given status.
+ *
+ * @param array $status_labels Set of statuses to find oldest & newest action for.
+ * @param array $action_counts Number of actions grouped by status.
+ * @param array $oldest_and_newest Date of the oldest and newest action with each status.
+ */
+ protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) {
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $count ) {
+ // WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column.
+ printf(
+ '
A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required.
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+ {% if site.google_analytics %}
+
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/includes/libraries/action-scheduler/docs/admin.md b/includes/libraries/action-scheduler/docs/admin.md
new file mode 100755
index 0000000..5ef0b56
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/admin.md
@@ -0,0 +1,22 @@
+---
+description: Learn how to administer background jobs with the Action Scheduler job queue for WordPress.
+---
+# Scheduled Actions Administration Screen
+
+Action Scheduler has a built in administration screen for monitoring, debugging and manually triggering scheduled actions.
+
+The administration interface is accesible through both:
+
+1. **Tools > Scheduled Actions**
+1. **WooCommerce > Status > Scheduled Actions**, when WooCommerce is installed.
+
+Among other tasks, from the admin screen you can:
+
+* run a pending action
+* view the scheduled actions with a specific status, like the all actions which have failed or are in-progress (https://cldup.com/NNTwE88Xl8.png).
+* view the log entries for a specific action to find out why it failed.
+* sort scheduled actions by hook name, scheduled date, claim ID or group name.
+
+Still have questions? Check out the [FAQ](/faq).
+
+
diff --git a/includes/libraries/action-scheduler/docs/android-chrome-192x192.png b/includes/libraries/action-scheduler/docs/android-chrome-192x192.png
new file mode 100755
index 0000000..86753db
Binary files /dev/null and b/includes/libraries/action-scheduler/docs/android-chrome-192x192.png differ
diff --git a/includes/libraries/action-scheduler/docs/android-chrome-256x256.png b/includes/libraries/action-scheduler/docs/android-chrome-256x256.png
new file mode 100755
index 0000000..3e5f9b1
Binary files /dev/null and b/includes/libraries/action-scheduler/docs/android-chrome-256x256.png differ
diff --git a/includes/libraries/action-scheduler/docs/api.md b/includes/libraries/action-scheduler/docs/api.md
new file mode 100755
index 0000000..a0bbb5d
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/api.md
@@ -0,0 +1,179 @@
+---
+description: Reference guide for background processing functions provided by the Action Scheduler job queue for WordPress.
+---
+# API Reference
+
+Action Scheduler provides a range of functions for scheduling hooks to run at some time in the future on one or more occassions.
+
+To understand the scheduling functoins, it can help to think of them as extensions to WordPress' `do_action()` function that add the ability to delay and repeat when the hook will be triggered.
+
+## WP-Cron APIs vs. Action Scheduler APIs
+
+The Action Scheduler API functions are designed to mirror the WordPress [WP-Cron API functions](http://codex.wordpress.org/Category:WP-Cron_Functions).
+
+Functions return similar values and accept similar arguments to their WP-Cron counterparts. The notable differences are:
+
+* `as_schedule_single_action()` & `as_schedule_recurring_action()` will return the post ID of the scheduled action rather than boolean indicating whether the event was scheduled
+* `as_schedule_recurring_action()` takes an interval in seconds as the recurring interval rather than an arbitrary string
+* `as_schedule_single_action()` & `as_schedule_recurring_action()` can accept a `$group` parameter to group different actions for the one plugin together.
+* the `wp_` prefix is substituted with `as_` and the term `event` is replaced with `action`
+
+## API Function Availability
+
+As mentioned in the [Usage - Load Order](/usage/#load-order) section, Action Scheduler will initialize itself on the `'init'` hook with priority `1`. While API functions are loaded prior to this and call be called, they should not be called until after `'init'` with priority `1`, because each component, like the data store, has not yet been initialized.
+
+Do not use Action Scheduler API functions prior to `'init'` hook with priority `1`. Doing so could lead to unexpected results, like data being stored in the incorrect location.
+
+## Function Reference / `as_schedule_single_action()`
+
+### Description
+
+Schedule an action to run one time.
+
+### Usage
+
+```php
+as_schedule_single_action( $timestamp, $hook, $args, $group )
+````
+
+### Parameters
+
+- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_.
+- **$hook** (string)(required) Name of the action hook. Default: _none_.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (array) The group to assign this job to. Default: _''_.
+
+### Return value
+
+(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table.
+
+
+## Function Reference / `as_schedule_recurring_action()`
+
+### Description
+
+Schedule an action to run repeatedly with a specified interval in seconds.
+
+### Usage
+
+```php
+as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group )
+````
+
+### Parameters
+
+- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_.
+- **$interval_in_seconds** (integer)(required) How long to wait between runs. Default: _none_.
+- **$hook** (string)(required) Name of the action hook. Default: _none_.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (array) The group to assign this job to. Default: _''_.
+
+### Return value
+
+(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table.
+
+
+## Function Reference / `as_schedule_cron_action()`
+
+### Description
+
+Schedule an action that recurs on a cron-like schedule.
+
+### Usage
+
+```php
+as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group )
+````
+
+### Parameters
+
+- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_.
+- **$schedule** (string)(required) $schedule A cron-link schedule string, see http://en.wikipedia.org/wiki/Cron. Default: _none_.
+- **$hook** (string)(required) Name of the action hook. Default: _none_.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (array) The group to assign this job to. Default: _''_.
+
+### Return value
+
+(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table.
+
+
+## Function Reference / `as_unschedule_action()`
+
+### Description
+
+Cancel the next occurrence of a job.
+
+### Usage
+
+```php
+as_unschedule_action( $hook, $args, $group )
+````
+
+### Parameters
+
+- **$hook** (string)(required) Name of the action hook. Default: _none_.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (array) The group to assign this job to. Default: _''_.
+
+### Return value
+
+(null)
+
+
+## Function Reference / `as_next_scheduled_action()`
+
+### Description
+
+Returns the next timestamp for a scheduled action.
+
+### Usage
+
+```php
+as_next_scheduled_action( $hook, $args, $group )
+````
+
+### Parameters
+
+- **$hook** (string)(required) Name of the action hook. Default: _none_.
+- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
+- **$group** (array) The group to assign this job to. Default: _''_.
+
+### Return value
+
+(integer|boolean) The timestamp for the next occurrence, or false if nothing was found.
+
+
+## Function Reference / `as_get_scheduled_actions()`
+
+### Description
+
+Find scheduled actions.
+
+### Usage
+
+```php
+as_get_scheduled_actions( $args, $return_format )
+````
+
+### Parameters
+
+- **$args** (array) Arguments to search and filter results by. Possible arguments, with their default values:
+ * `'hook' => ''` - the name of the action that will be triggered
+ * `'args' => NULL` - the args array that will be passed with the action
+ * `'date' => NULL` - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime().
+ * `'date_compare' => '<=`' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='
+ * `'modified' => NULL` - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime().
+ * `'modified_compare' => '<='` - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='
+ * `'group' => ''` - the group the action belongs to
+ * `'status' => ''` - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING
+ * `'claimed' => NULL` - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID
+ * `'per_page' => 5` - Number of results to return
+ * `'offset' => 0`
+ * `'orderby' => 'date'` - accepted values are 'hook', 'group', 'modified', or 'date'
+ * `'order' => 'ASC'`
+- **$return_format** (string) The format in which to return the scheduled actions: 'OBJECT', 'ARRAY_A', or 'ids'. Default: _'OBJECT'_.
+
+### Return value
+
+(array) Array of the actions matching the criteria specified with `$args`.
diff --git a/includes/libraries/action-scheduler/docs/apple-touch-icon.png b/includes/libraries/action-scheduler/docs/apple-touch-icon.png
new file mode 100755
index 0000000..1bc4975
Binary files /dev/null and b/includes/libraries/action-scheduler/docs/apple-touch-icon.png differ
diff --git a/includes/libraries/action-scheduler/docs/assets/css/style.scss b/includes/libraries/action-scheduler/docs/assets/css/style.scss
new file mode 100755
index 0000000..67fe6b6
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/assets/css/style.scss
@@ -0,0 +1,32 @@
+---
+---
+
+@import "{{ site.theme }}";
+
+a {
+ text-shadow: none;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+header h1 a {
+ color: #b5e853;
+}
+
+.container {
+ max-width: 700px;
+}
+
+footer {
+ margin-top: 6em;
+ padding: 1.6em 0;
+ border-top: 1px dashed #b5e853;
+}
+
+.footer-image {
+ text-align: center;
+ padding-top: 1em;
+}
\ No newline at end of file
diff --git a/includes/libraries/action-scheduler/docs/browserconfig.xml b/includes/libraries/action-scheduler/docs/browserconfig.xml
new file mode 100755
index 0000000..f6244e6
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #151515
+
+
+
diff --git a/includes/libraries/action-scheduler/docs/faq.md b/includes/libraries/action-scheduler/docs/faq.md
new file mode 100755
index 0000000..38f07f5
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/faq.md
@@ -0,0 +1,101 @@
+## FAQ
+
+### Is it safe to release Action Scheduler in my plugin? Won't its functions conflict with another copy of the library?
+
+Action Scheduler is designed to be used and released in plugins. It avoids redeclaring public API functions when more than one copy of the library is being loaded by different plugins. It will also load only the most recent version of itself (by checking registered versions after all plugins are loaded on the `'plugins_loaded'` hook).
+
+To use it in your plugin, simply require the `action-scheduler/action-scheduler.php` file. Action Scheduler will take care of the rest.
+
+### I don't want to use WP-Cron. Does Action Scheduler depend on WP-Cron?
+
+By default, Action Scheduler is initiated by WP-Cron. However, it has no dependency on the WP-Cron system. You can initiate the Action Scheduler queue in other ways with just one or two lines of code.
+
+For example, you can start a queue directly by calling:
+
+```php
+ActionScheduler::runner()->run();
+```
+
+Or trigger the `'action_scheduler_run_queue'` hook and let Action Scheduler do it for you:
+
+```php
+do_action( 'action_scheduler_run_queue' );
+```
+
+Further customization can be done by extending the `ActionScheduler_Abstract_QueueRunner` class to create a custom Queue Runner. For an example of a customized queue runner, see the [`ActionScheduler_WPCLI_QueueRunner`](https://github.com/Prospress/action-scheduler/blob/master/classes/ActionScheduler_WPCLI_QueueRunner.php), which is used when running WP CLI.
+
+Want to create some other method for initiating Action Scheduler? [Open a new issue](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.
+
+### I don't want to use WP-Cron, ever. Does Action Scheduler replace WP-Cron?
+
+By default, Action Scheduler is designed to work alongside WP-Cron and not change any of its behaviour. This helps avoid unexpectedly overriding WP-Cron on sites installing your plugin, which may have nothing to do with WP-Cron.
+
+However, we can understand why you might want to replace WP-Cron completely in environments within you control, especially as it gets you the advantages of Action Scheduler. This should be possible without too much code.
+
+You could use the `'schedule_event'` hook in WordPress to use Action Scheduler for only newly scheduled WP-Cron jobs and map the `$event` param to Action Scheduler API functions.
+
+Alternatively, you can use a combination of the `'pre_update_option_cron'` and `'pre_option_cron'` hooks to override all new and previously scheduled WP-Cron jobs (similar to the way [Cavalcade](https://github.com/humanmade/Cavalcade) does it).
+
+If you'd like to create a plugin to do this automatically and want to share your work with others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.
+
+### Eww gross, Custom Post Types! That's _so_ 2010. Can I use a different storage scheme?
+
+Of course! Action Scheduler data storage is completely swappable, and always has been.
+
+You can store scheduled actions in custom tables in the WordPress site's database. Some sites using it already are. You can actually store them anywhere for that matter, like in a remote storage service from Amazon Web Services.
+
+To implement a custom store:
+
+1. extend the abstract `ActionScheduler_Store` class, being careful to implement each of its methods
+2. attach a callback to `'action_scheduler_store_class'` to tell Action Scheduler your class is the one which should be used to manage storage, e.g.
+
+```
+function eg_define_custom_store( $existing_storage_class ) {
+ return 'My_Radical_Action_Scheduler_Store';
+}
+add_filter( 'action_scheduler_store_class', 'eg_define_custom_store', 10, 1 );
+```
+
+Take a look at the `ActionScheduler_wpPostStore` class for an example implementation of `ActionScheduler_Store`.
+
+If you'd like to create a plugin to do this automatically and release it publicly to help others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.
+
+> Note: we're also moving Action Scheduler itself to use [custom tables for better scalability](https://github.com/Prospress/action-scheduler/issues/77).
+
+### Can I use a different storage scheme just for logging?
+
+Of course! Action Scheduler's logger is completely swappable, and always has been. You can also customise where logs are stored, and the storage mechanism.
+
+To implement a custom logger:
+
+1. extend the abstract `ActionScheduler_Logger` class, being careful to implement each of its methods
+2. attach a callback to `'action_scheduler_logger_class'` to tell Action Scheduler your class is the one which should be used to manage logging, e.g.
+
+```
+function eg_define_custom_logger( $existing_storage_class ) {
+ return 'My_Radical_Action_Scheduler_Logger';
+}
+add_filter( 'action_scheduler_logger_class', 'eg_define_custom_logger', 10, 1 );
+```
+
+Take a look at the `ActionScheduler_wpCommentLogger` class for an example implementation of `ActionScheduler_Logger`.
+
+### I want to run Action Scheduler only on a dedicated application server in my cluster. Can I do that?
+
+Wow, now you're really asking the tough questions. In theory, yes, this is possible. The `ActionScheduler_QueueRunner` class, which is responsible for running queues, is swappable via the `'action_scheduler_queue_runner_class'` filter.
+
+Because of this, you can effectively customise queue running however you need. Whether that means tweaking minor things, like not using WP-Cron at all to initiate queues by overriding `ActionScheduler_QueueRunner::init()`, or completely changing how and where queues are run, by overriding `ActionScheduler_QueueRunner::run()`.
+
+### Is Action Scheduler safe to use on my production site?
+
+Yes, absolutely! Action Scheduler is actively used on tens of thousands of production sites already. Right now it's responsible for scheduling everything from emails to payments.
+
+In fact, every month, Action Scheduler processes millions of payments as part of the [WooCommerce Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/) extension.
+
+It requires no setup, and won't override any WordPress APIs (unless you want it to).
+
+### How does Action Scheduler work on WordPress Multisite?
+
+Action Scheduler is designed to manage the scheduled actions on a single site. It has no special handling for running queues across multiple sites in a multisite network. That said, because it's storage and Queue Runner are completely swappable, it would be possible to write multisite handling classes to use with it.
+
+If you'd like to create a multisite plugin to do this and release it publicly to help others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.
diff --git a/includes/libraries/action-scheduler/docs/favicon-16x16.png b/includes/libraries/action-scheduler/docs/favicon-16x16.png
new file mode 100755
index 0000000..aa772d1
Binary files /dev/null and b/includes/libraries/action-scheduler/docs/favicon-16x16.png differ
diff --git a/includes/libraries/action-scheduler/docs/favicon-32x32.png b/includes/libraries/action-scheduler/docs/favicon-32x32.png
new file mode 100755
index 0000000..773fb30
Binary files /dev/null and b/includes/libraries/action-scheduler/docs/favicon-32x32.png differ
diff --git a/includes/libraries/action-scheduler/docs/favicon.ico b/includes/libraries/action-scheduler/docs/favicon.ico
new file mode 100755
index 0000000..3cdbb51
Binary files /dev/null and b/includes/libraries/action-scheduler/docs/favicon.ico differ
diff --git a/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html b/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html
new file mode 100755
index 0000000..f3bf171
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html
@@ -0,0 +1 @@
+google-site-verification: google14ef723abb376cd3.html
\ No newline at end of file
diff --git a/includes/libraries/action-scheduler/docs/index.md b/includes/libraries/action-scheduler/docs/index.md
new file mode 100755
index 0000000..989f262
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/index.md
@@ -0,0 +1,68 @@
+---
+title: Action Scheduler - Background Processing Job Queue for WordPress
+---
+## WordPress Job Queue with Background Processing
+
+Action Scheduler is a library for triggering a WordPress hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occassions.
+
+Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook.
+
+It just so happens, this functionality also creates a robust job queue for background processing large queues of tasks in WordPress. With the additional of logging and an [administration interface](/admin/), that also provide tracability on your tasks processed in the background.
+
+### Battle-Tested Background Processing
+
+Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins.
+
+It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, in 10 concurrent queues at a rate of over 10,000 actions / hour without negatively impacting normal site operations.
+
+This is all possible on infrastructure and WordPress sites outside the control of the plugin author.
+
+Action Scheduler is specifically designed for distribution in WordPress plugins (and themes) - no server access required. If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help.
+
+### How it Works
+
+Action Scheduler uses a WordPress [custom post type](http://codex.wordpress.org/Post_Types), creatively named `scheduled-action`, to store the hook name, arguments and scheduled date for an action that should be triggered at some time in the future.
+
+The scheduler will attempt to run every minute by attaching itself as a callback to the `'action_scheduler_run_schedule'` hook, which is scheduled using WordPress's built-in [WP-Cron](http://codex.wordpress.org/Function_Reference/wp_cron) system.
+
+When triggered, Action Scheduler will check for posts of the `scheduled-action` type that have a `post_date` at or before this point in time i.e. actions scheduled to run now or at sometime in the past.
+
+### Batch Processing Background Jobs
+
+If there are actions to be processed, Action Scheduler will stake a unique claim for a batch of 20 actions and begin processing that batch. The PHP process spawned to run the batch will then continue processing batches of 20 actions until it times out or exhausts available memory.
+
+If your site has a large number of actions scheduled to run at the same time, Action Scheduler will process more than one batch at a time. Specifically, when the `'action_scheduler_run_schedule'` hook is triggered approximately one minute after the first batch began processing, a new PHP process will stake a new claim to a batch of actions which were not claimed by the previous process. It will then begin to process that batch.
+
+This will continue until all actions are processed using a maximum of 5 concurrent queues.
+
+### Housekeeping
+
+Before processing a batch, the scheduler will remove any existing claims on actions which have been sitting in a queue for more than five minutes.
+
+Action Scheduler will also trash any actions which were completed more than a month ago.
+
+If an action runs for more than 5 minutes, Action Scheduler will assume the action has timed out and will mark it as failed. However, if all callbacks attached to the action were to successfully complete sometime after that 5 minute timeout, its status would later be updated to completed.
+
+### Traceable Background Processing
+
+Did your background job run?
+
+Never be left wondering with Action Scheduler's built-in record keeping.
+
+All events for each action are logged in the [comments table](http://codex.wordpress.org/Database_Description#Table_Overview) and displayed in the [administration interface](/admin/).
+
+The events logged by default include when an action:
+ * is created
+ * starts
+ * completes
+ * fails
+
+If it fails with an error that can be recorded, that error will be recorded in the log and visible in administration interface, making it possible to trace what went wrong at some point in the past on a site you didn't have access to in the past.
+
+Actions can also be grouped together using a custom taxonomy named `action-group`.
+
+## Credits
+
+Developed and maintained by [Prospress](http://prospress.com/) in collaboration with [Flightless](https://flightless.us/).
+
+Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/prospress/action-scheduler/pulls) welcome.
\ No newline at end of file
diff --git a/includes/libraries/action-scheduler/docs/mstile-150x150.png b/includes/libraries/action-scheduler/docs/mstile-150x150.png
new file mode 100755
index 0000000..3a3ed19
Binary files /dev/null and b/includes/libraries/action-scheduler/docs/mstile-150x150.png differ
diff --git a/includes/libraries/action-scheduler/docs/perf.md b/includes/libraries/action-scheduler/docs/perf.md
new file mode 100755
index 0000000..542792d
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/perf.md
@@ -0,0 +1,127 @@
+---
+title: WordPress Background Processing at Scale - Action Scheduler Job Queue
+description: Learn how to do WordPress background processing at scale by tuning the Action Scheduler job queue's default WP Cron runner.
+---
+# Background Processing at Scale
+
+Action Scheduler's default processing is designed to work reliably across all different hosting environments. In order to achieve that, the default processing thresholds are very conservative.
+
+Specifically, Action Scheduler will only process actions until:
+
+* 90% of available memory is used
+* processing another 3 actions would exceed 30 seconds of total request time, based on the average processing time for the current batch
+
+On sites with large queues, this can result in very slow processing time.
+
+While using [WP CLI to process queues](/wp-cli/) is the best approach to increasing processing speed, on occasion, that is not a viable option. In these cases, it's also possible to increase the processing thresholds in Action Scheduler to increase the rate at which actions are processed by the default WP Cron queue runner.
+
+## Increasing Time Limit
+
+By default, Action Scheduler will only process actions for a maximum of 30 seconds. This time limit minimises the risk of a script timeout on unknown hosting environments, some of which enforce 30 second timeouts.
+
+If you know your host supports longer than this time limit for web requests, you can increase this time limit. This allows more actions to be processed in each request and reduces the lag between processing each queue, greating speeding up the processing rate of scheduled actions.
+
+For example, the following snippet will increase the timelimit to 2 minutes (120 seconds):
+
+```php
+function eg_increase_time_limit( $time_limit ) {
+ return 120;
+}
+add_filter( 'action_scheduler_queue_runner_time_limit', 'eg_increase_time_limit' );
+```
+
+Some of the known host time limits are:
+
+* 60 second on WP Engine
+* 120 seconds on Pantheon
+* 120 seconds on SiteGround
+
+## Increasing Batch Size
+
+By default, Action Scheduler will claim a batch of 25 actions. This small batch size is because the default time limit is only 30 seconds; however, if you know your actions are processing very quickly, e.g. taking microseconds not seconds, or that you have more than 30 second available to process each batch, increasing the batch size can improve performance.
+
+This is because claiming a batch has some overhead, so the less often a batch needs to be claimed, the faster actions can be processed.
+
+For example, to increase the batch size to 100, we can use the following function:
+
+```php
+function eg_increase_action_scheduler_batch_size( $batch_size ) {
+ return 100;
+}
+add_filter( 'action_scheduler_queue_runner_batch_size', 'eg_increase_action_scheduler_batch_size' );
+```
+
+## Increasing Concurrent Batches
+
+By default, Action Scheduler will run up to 5 concurrent batches of actions. This is to prevent consuming all the available connections or processes on your webserver.
+
+However, your server may allow a large number of connection, for example, because it has a high value for Apache's `MaxClients` setting or PHP-FPM's `pm.max_children` setting.
+
+If this is the case, you can use the `'action_scheduler_queue_runner_concurrent_batches'` filter to increase the number of conncurrent batches allowed, and therefore speed up processing large numbers of actions scheduled to be processed simultaneously.
+
+For example, to increase the allowed number of concurrent queues to 10, we can use the following code:
+
+```php
+function eg_increase_action_scheduler_concurrent_batches( $concurrent_batches ) {
+ return 10;
+}
+add_filter( 'action_scheduler_queue_runner_concurrent_batches', 'eg_increase_action_scheduler_concurrent_batches' );
+```
+
+## Increasing Initialisation Rate of Runners
+
+By default, Action scheduler initiates at most, one queue runner every time the `'action_scheduler_run_queue'` action is triggered by WP Cron.
+
+Because this action is only triggered at most once every minute, if a queue is only allowed to process for one minute, then there will never be more than one queue processing actions, greatly reducing the processing rate.
+
+To handle larger queues on more powerful servers, it's a good idea to initiate additional queue runners whenever the `'action_scheduler_run_queue'` action is run.
+
+That can be done by initiated additional secure requests to our server via loopback requests.
+
+The code below demonstrates how to create 5 loopback requests each time a queue begins
+
+```php
+/**
+ * Trigger 5 additional loopback requests with unique URL params.
+ */
+function eg_request_additional_runners() {
+
+ // allow self-signed SSL certificates
+ add_filter( 'https_local_ssl_verify', '__return_false', 100 );
+
+ for ( $i = 0; $i < 5; $i++ ) {
+ $response = wp_remote_post( admin_url( 'admin-ajax.php' ), array(
+ 'method' => 'POST',
+ 'timeout' => 45,
+ 'redirection' => 5,
+ 'httpversion' => '1.0',
+ 'blocking' => false,
+ 'headers' => array(),
+ 'body' => array(
+ 'action' => 'eg_create_additional_runners',
+ 'instance' => $i,
+ 'eg_nonce' => wp_create_nonce( 'eg_additional_runner_' . $i ),
+ ),
+ 'cookies' => array(),
+ ) );
+ }
+}
+add_action( 'action_scheduler_run_queue', 'eg_request_additional_runners', 0 );
+
+/**
+ * Handle requests initiated by eg_request_additional_runners() and start a queue runner if the request is valid.
+ */
+function eg_create_additional_runners() {
+
+ if ( isset( $_POST['eg_nonce'] ) && isset( $_POST['instance'] ) && wp_verify_nonce( $_POST['eg_nonce'], 'eg_additional_runner_' . $_POST['instance'] ) ) {
+ ActionScheduler_QueueRunner::instance()->run();
+ }
+
+ wp_die();
+}
+add_action( 'wp_ajax_nopriv_eg_create_additional_runners', 'eg_create_additional_runners', 0 );
+```
+
+## High Volume Plugin
+
+It's not necessary to add all of this code yourself, the folks at [Prospress](https://prospress.com) have created a handy plugin to get access to each of these increases - the [Action Scheduler - High Volume](https://github.com/prospress/action-scheduler-high-volume) plugin.
diff --git a/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg b/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg
new file mode 100755
index 0000000..b67c32b
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg
@@ -0,0 +1,40 @@
+
+
+
diff --git a/includes/libraries/action-scheduler/docs/site.webmanifest b/includes/libraries/action-scheduler/docs/site.webmanifest
new file mode 100755
index 0000000..de65106
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-256x256.png",
+ "sizes": "256x256",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/includes/libraries/action-scheduler/docs/usage.md b/includes/libraries/action-scheduler/docs/usage.md
new file mode 100755
index 0000000..dda4ed7
--- /dev/null
+++ b/includes/libraries/action-scheduler/docs/usage.md
@@ -0,0 +1,123 @@
+---
+description: Learn how to use the Action Scheduler background processing job queue for WordPress in your WordPress plugin.
+---
+# Usage
+
+Using Action Scheduler requires:
+
+1. installing the library
+1. scheduling and action
+1. attaching a callback to that action
+
+## Scheduling an Action
+
+To schedule an action, call the [API function](/api/) for the desired schedule type passing in the required parameters.
+
+The example code below shows everything needed to schedule a function to run at midnight, if it's not already scheduled:
+
+```php
+require_once( plugin_dir_path( __FILE__ ) . '/libraries/action-scheduler/action-scheduler.php' );
+
+/**
+ * Schedule an action with the hook 'eg_midnight_log' to run at midnight each day
+ * so that our callback is run then.
+ */
+function eg_log_action_data() {
+ if ( false === as_next_scheduled_action( 'eg_midnight_log' ) ) {
+ as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, 'eg_midnight_log' );
+ }
+}
+add_action( 'init', 'eg_log_action_data' );
+
+/**
+ * A callback to run when the 'eg_midnight_log' scheduled action is run.
+ */
+function eg_log_action_data() {
+ error_log( 'It is just after midnight on ' . date( 'Y-m-d' ) );
+}
+add_action( 'eg_midnight_log', 'eg_log_action_data' );
+```
+
+For more details on all available API functions, and the data they accept, refer to the [API Reference](/api/).
+
+## Installation
+
+There are two ways to install Action Scheduler:
+
+1. regular WordPress plugin; or
+1. a library within your plugin's codebase.
+
+### Usage as a Plugin
+
+Action Scheduler includes the necessary file headers to be used as a standard WordPress plugin.
+
+To install it as a plugin:
+
+1. Download the .zip archive of the latest [stable release](https://github.com/Prospress/action-scheduler/releases)
+1. Go to the **Plugins > Add New > Upload** administration screen on your WordPress site
+1. Select the archive file you just downloaded
+1. Click **Install Now**
+1. Click **Activate**
+
+Or clone the Git repository into your site's `wp-content/plugins` folder.
+
+Using Action Scheduler as a plugin can be handy for developing against newer versions, rather than having to update the subtree in your codebase. **When installed as a plugin, Action Scheduler does not provide any user interfaces for scheduling actions**. The only way to interact with Action Scheduler is via code.
+
+### Usage as a Library
+
+To use Action Scheduler as a library:
+
+1. include the Action Scheduler codebase
+1. load the library by including the `action-scheduler.php` file
+
+Using a [subtree in your plugin, theme or site's Git repository](https://www.atlassian.com/blog/git/alternatives-to-git-submodule-git-subtree) to include Action Scheduler is the recommended method. Composer can also be used.
+
+To include Action Scheduler as a git subtree:
+
+#### Step 1. Add the Repository as a Remote
+
+```
+git remote add -f subtree-action-scheduler https://github.com/Prospress/action-scheduler.git
+```
+
+Adding the subtree as a remote allows us to refer to it in short from via the name `subtree-action-scheduler`, instead of the full GitHub URL.
+
+#### Step 2. Add the Repo as a Subtree
+
+```
+git subtree add --prefix libraries/action-scheduler subtree-action-scheduler master --squash
+```
+
+This will add the `master` branch of Action Scheduler to your repository in the folder `libraries/action-scheduler`.
+
+You can change the `--prefix` to change where the code is included. Or change the `master` branch to a tag, like `2.1.0` to include only a stable version.
+
+#### Step 3. Update the Subtree
+
+To update Action Scheduler to a new version, use the commands:
+
+```
+git fetch subtree-action-scheduler master
+git subtree pull --prefix libraries/action-scheduler subtree-action-scheduler master --squash
+```
+
+### Loading Action Scheduler
+
+Regardless of how it is installed, to load Action Scheduler, you only need to include the `action-scheduler.php` file, e.g.
+
+```php
+ $batch_size,
- 'subscription_status' => wcs_get_subscription_ended_statuses(),
+ 'subscription_status' => $subscription_ended_statuses,
'meta_query' => array(
array(
'key' => '_schedule_end',
diff --git a/includes/upgrades/class-wc-subscriptions-upgrader.php b/includes/upgrades/class-wc-subscriptions-upgrader.php
index fa54ae9..fe69a7d 100755
--- a/includes/upgrades/class-wc-subscriptions-upgrader.php
+++ b/includes/upgrades/class-wc-subscriptions-upgrader.php
@@ -101,9 +101,6 @@ class WC_Subscriptions_Upgrader {
// When WC is updated from a version prior to 3.0 to a version after 3.0, add subscription address indexes. Must be hooked on before WC runs its updates, which occur on priority 5.
add_action( 'init', array( __CLASS__, 'maybe_add_subscription_address_indexes' ), 2 );
- // Hooks into WC's wc_update_350_order_customer_id upgrade routine.
- add_action( 'init', array( __CLASS__, 'maybe_update_subscription_post_author' ), 2 );
-
add_action( 'admin_notices', array( __CLASS__, 'maybe_add_downgrade_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'maybe_display_external_object_cache_warning' ) );
@@ -238,6 +235,11 @@ class WC_Subscriptions_Upgrader {
self::$background_updaters['2.4']['start_date_metadata']->schedule_repair();
}
+ // Upon upgrading or installing 2.5.0 for the first time, enable or disable PayPal Standard for Subscriptions.
+ if ( version_compare( self::$active_version, '2.5.0', '<' ) ) {
+ WCS_PayPal::set_enabled_for_subscriptions_default();
+ }
+
self::upgrade_complete();
}
@@ -368,8 +370,8 @@ class WC_Subscriptions_Upgrader {
$results = array(
'upgraded_count' => 0,
- // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag
- 'message' => sprintf( __( 'Unable to upgrade subscriptions. Error: %1$s Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '' ),
+ // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag
+ 'message' => sprintf( __( 'Unable to upgrade subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '', ' ' ),
'status' => 'error',
);
}
@@ -415,8 +417,8 @@ class WC_Subscriptions_Upgrader {
$results = array(
'repaired_count' => 0,
'unrepaired_count' => 0,
- // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag
- 'message' => sprintf( _x( 'Unable to repair subscriptions. Error: %1$s Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '' ),
+ // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag
+ 'message' => sprintf( _x( 'Unable to repair subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '', ' ' ),
'status' => 'error',
);
}
@@ -826,24 +828,6 @@ class WC_Subscriptions_Upgrader {
}
}
- /**
- * Handles the WC 3.5.0 upgrade routine that moves customer IDs from post metadata to the 'post_author' column.
- *
- * @since 2.4.0
- */
- public static function maybe_update_subscription_post_author() {
- if ( version_compare( WC()->version, '3.5.0', '<' ) ) {
- return;
- }
-
- // If WC hasn't run the update routine yet we can hook into theirs to update subscriptions, otherwise we'll need to schedule our own update.
- if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '<' ) ) {
- self::$background_updaters['2.4']['subscription_post_author']->hook_into_wc_350_update();
- } else if ( version_compare( self::$active_version, '2.4.0', '<' ) ) {
- self::$background_updaters['2.4']['subscription_post_author']->schedule_repair();
- }
- }
-
/**
* Load and initialise the background updaters.
*
@@ -854,7 +838,6 @@ class WC_Subscriptions_Upgrader {
self::$background_updaters['2.3']['suspended_paypal_repair'] = new WCS_Repair_Suspended_PayPal_Subscriptions( $logger );
self::$background_updaters['2.3']['address_indexes_repair'] = new WCS_Repair_Subscription_Address_Indexes( $logger );
self::$background_updaters['2.4']['start_date_metadata'] = new WCS_Repair_Start_Date_Metadata( $logger );
- self::$background_updaters['2.4']['subscription_post_author'] = new WCS_Upgrade_Subscription_Post_Author( $logger );
// Init the updaters
foreach ( self::$background_updaters as $version => $updaters ) {
@@ -903,6 +886,27 @@ class WC_Subscriptions_Upgrader {
$admin_notice->display();
}
+ /**
+ * Handles the WC 3.5.0 upgrade routine that moves customer IDs from post metadata to the 'post_author' column.
+ *
+ * @since 2.4.0
+ * @deprecated 2.5.0
+ */
+ public static function maybe_update_subscription_post_author() {
+ wcs_deprecated_function( __METHOD__, '2.5.0' );
+
+ if ( version_compare( WC()->version, '3.5.0', '<' ) ) {
+ return;
+ }
+
+ // If WC hasn't run the update routine yet we can hook into theirs to update subscriptions, otherwise we'll need to schedule our own update.
+ if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '<' ) ) {
+ self::$background_updaters['2.4']['subscription_post_author']->hook_into_wc_350_update();
+ } else if ( version_compare( self::$active_version, '2.4.0', '<' ) ) {
+ self::$background_updaters['2.4']['subscription_post_author']->schedule_repair();
+ }
+ }
+
/**
* Used to check if a user ID is greater than the last user upgraded to version 1.4.
*
diff --git a/includes/upgrades/class-wcs-upgrade-logger.php b/includes/upgrades/class-wcs-upgrade-logger.php
index 61132fd..2191803 100755
--- a/includes/upgrades/class-wcs-upgrade-logger.php
+++ b/includes/upgrades/class-wcs-upgrade-logger.php
@@ -25,7 +25,8 @@ class WCS_Upgrade_Logger {
public static function init() {
- add_action( 'woocommerce_subscriptions_upgraded', __CLASS__ . '::schedule_cleanup', 10, 2 );
+ add_action( 'woocommerce_subscriptions_upgraded', array( __CLASS__, 'schedule_cleanup' ), 10, 2 );
+ add_action( 'woocommerce_subscriptions_upgraded', array( __CLASS__, 'add_more_info' ), 10 );
}
/**
@@ -63,6 +64,34 @@ class WCS_Upgrade_Logger {
}
}
+ /**
+ * Log more information during upgrade: Information about environment and active plugins
+ *
+ * @since 2.5.0
+ */
+ public static function add_more_info() {
+ global $wp_version;
+
+ self::add( sprintf( 'Environment info:' ) );
+ self::add( sprintf( ' WordPress Version : %s', $wp_version ) );
+
+ $active_plugins = get_option( 'active_plugins' );
+
+ // Check if get_plugins() function exists. This is required on the front end of the site.
+ if ( ! function_exists( 'get_plugins' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ $all_plugins = get_plugins();
+ self::add( sprintf( 'Active Plugins:' ) );
+
+ foreach ( $active_plugins as $plugin ) {
+ $author = empty( $all_plugins[ $plugin ]['Author'] ) ? 'Unknown' : $all_plugins[ $plugin ]['Author'];
+ $version = empty( $all_plugins[ $plugin ]['Version'] ) ? 'Unknown version' : $all_plugins[ $plugin ]['Version'];
+ self::add( sprintf( ' %s by %s – %s', $all_plugins[ $plugin ]['Name'], $author, $version ) );
+ }
+ }
+
/**
* Schedule a hook to automatically clear the log after 8 weeks
*/
@@ -71,3 +100,4 @@ class WCS_Upgrade_Logger {
self::add( sprintf( '%s upgrade complete from Subscriptions v%s while WooCommerce WC_VERSION %s and database version %s was active.', $current_version, $old_version, $wc_version, get_option( 'woocommerce_db_version' ) ) );
}
}
+
diff --git a/includes/upgrades/class-wcs-upgrade-notice-manager.php b/includes/upgrades/class-wcs-upgrade-notice-manager.php
index c143f54..4f4fdd7 100755
--- a/includes/upgrades/class-wcs-upgrade-notice-manager.php
+++ b/includes/upgrades/class-wcs-upgrade-notice-manager.php
@@ -19,7 +19,7 @@ class WCS_Upgrade_Notice_Manager {
*
* @var string
*/
- protected static $version = '2.3.0';
+ protected static $version = '2.5.0';
/**
* The number of times the notice will be displayed before being dismissed automatically.
@@ -77,26 +77,25 @@ class WCS_Upgrade_Notice_Manager {
return;
}
- $version = _x( '2.3', 'plugin version number used in admin notice', 'woocommerce-subscriptions' );
+ $version = _x( '2.5', 'plugin version number used in admin notice', 'woocommerce-subscriptions' );
$dismiss_url = wp_nonce_url( add_query_arg( 'dismiss_upgrade_notice', self::$version ), 'dismiss_upgrade_notice', '_wcsnonce' );
$notice = new WCS_Admin_Notice( 'notice notice-info', array(), $dismiss_url );
$features = array(
array(
- 'title' => __( 'New Subscription Coupon Features', 'woocommerce-subscriptions' ),
- 'description' => __( 'Want to offer customers coupons which apply for 6 months? You can now define the number of cycles discounts would be applied.', 'woocommerce-subscriptions' ),
+ 'title' => __( 'New options to allow customers to sign up without a credit card', 'woocommerce-subscriptions' ),
+ 'description' => __( 'Allow customers to access free trial and other $0 subscription products without needing to enter their credit card details on sign up.', 'woocommerce-subscriptions' ),
),
array(
- 'title' => __( 'New Signup Pricing Options for Synchronized Subscriptions', 'woocommerce-subscriptions' ),
- 'description' => __( 'Charge the full recurring price at the time of sign up for synchronized subscriptions. Your customers can now receive their products straight away.', 'woocommerce-subscriptions' ),
+ 'title' => __( 'Improved subscription payment method information', 'woocommerce-subscriptions' ),
+ 'description' => __( 'Customers can now see more information about what payment method will be used for future payments.', 'woocommerce-subscriptions' ),
),
array(
- 'title' => __( 'Link Parent Orders to Subscriptions', 'woocommerce-subscriptions' ),
- // translators: placeholders are opening and closing tags linking to documentation.
- 'description' => sprintf( __( 'For subscriptions with no parent order, shop managers can now choose a parent order via the Edit Subscription screen. This makes it possible to set a parent order on %smanually created subscriptions%s. The order can also be sent to customers to act as an invoice that needs to be paid to activate the subscription.', 'woocommerce-subscriptions' ), '', '' ),
+ 'title' => __( 'Auto-renewal toggle', 'woocommerce-subscriptions' ),
+ 'description' => sprintf( __( 'Enabled via a setting, this new feature will allow your customers to turn on and off automatic payments from the %sMy Account > View Subscription%s pages.', 'woocommerce-subscriptions' ), '', '' ),
),
array(
- 'title' => __( 'Early Renewal', 'woocommerce-subscriptions' ),
- 'description' => __( 'Customers can now renew their subscriptions before the scheduled next payment date. Why not use this to email your customers a coupon a month before their annual subscription renewals to get access to that revenue sooner?', 'woocommerce-subscriptions' ),
+ 'title' => __( 'Update all subscription payment methods', 'woocommerce-subscriptions' ),
+ 'description' => __( "Customers will now have the option to update all their subscriptions when they are changing one of their subscription's payment methods - provided the payment gateway supports it.", 'woocommerce-subscriptions' ),
),
);
@@ -109,7 +108,7 @@ class WCS_Upgrade_Notice_Manager {
$notice->set_actions( array(
array(
'name' => __( 'Learn More', 'woocommerce-subscriptions' ),
- 'url' => 'https://docs.woocommerce.com/document/subscriptions/version-2-3/',
+ 'url' => 'https://docs.woocommerce.com/document/subscriptions/version-2-5/',
),
) );
diff --git a/includes/upgrades/class-wcs-upgrade-subscription-post-author.php b/includes/upgrades/class-wcs-upgrade-subscription-post-author.php
index 89c3a9c..7446f75 100755
--- a/includes/upgrades/class-wcs-upgrade-subscription-post-author.php
+++ b/includes/upgrades/class-wcs-upgrade-subscription-post-author.php
@@ -2,10 +2,11 @@
/**
* Updates the 'post_author' column for subscriptions on WC 3.5+.
*
- * @author Prospress
- * @category Admin
- * @package WooCommerce Subscriptions/Admin/Upgrades
- * @version 2.4.0
+ * @author Prospress
+ * @category Admin
+ * @package WooCommerce Subscriptions/Admin/Upgrades
+ * @version 2.4.0
+ * @deprecated 2.5.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -21,6 +22,8 @@ class WCS_Upgrade_Subscription_Post_Author extends WCS_Background_Upgrader {
* @since 2.4.0
*/
public function __construct( WC_Logger $logger ) {
+ wcs_deprecated_function( __METHOD__, '2.5.0' );
+
$this->scheduled_hook = 'wcs_upgrade_subscription_post_author';
$this->log_handle = 'wcs-upgrade-subscription-post-author';
$this->logger = $logger;
diff --git a/includes/upgrades/templates/wcs-upgrade.php b/includes/upgrades/templates/wcs-upgrade.php
index dd4730f..0d94ae3 100755
--- a/includes/upgrades/templates/wcs-upgrade.php
+++ b/includes/upgrades/templates/wcs-upgrade.php
@@ -40,7 +40,7 @@ if ( ! defined( 'ABSPATH' ) ) {
-
+
@@ -50,7 +50,7 @@ if ( ! defined( 'ABSPATH' ) ) {
20 ) : ?>
-
+
diff --git a/includes/wcs-cart-functions.php b/includes/wcs-cart-functions.php
index f263017..128b2ff 100755
--- a/includes/wcs-cart-functions.php
+++ b/includes/wcs-cart-functions.php
@@ -192,7 +192,7 @@ function wcs_cart_totals_shipping_method_price_label( $method, $cart ) {
$price_label .= _x( 'Free', 'shipping method price', 'woocommerce-subscriptions' );
}
- return $price_label;
+ return apply_filters( 'wcs_cart_totals_shipping_method_price_label', $price_label, $method, $cart );
}
/**
@@ -305,7 +305,7 @@ function wcs_cart_price_string( $recurring_amount, $cart ) {
'subscription_interval' => wcs_cart_pluck( $cart, 'subscription_period_interval' ),
'subscription_period' => wcs_cart_pluck( $cart, 'subscription_period', '' ),
'subscription_length' => wcs_cart_pluck( $cart, 'subscription_length' ),
- ) ) );
+ ), $cart ) );
}
/**
diff --git a/includes/wcs-compatibility-functions.php b/includes/wcs-compatibility-functions.php
index 4ca95b8..41b0c76 100755
--- a/includes/wcs-compatibility-functions.php
+++ b/includes/wcs-compatibility-functions.php
@@ -54,39 +54,36 @@ function wcs_help_tip( $tip, $allow_html = false ) {
* returned as MySQL strings in the site's timezone. We return them from here as MySQL strings in UTC timezone because that's how
* dates are used in Subscriptions in almost all cases, for sanity's sake.
*
- * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access.
- * @param string $property The property name.
- * @param string $single Whether to return just the first piece of meta data with the given property key, or all meta data.
- * @param mixed $default (optional) The value to return if no value is found - defaults to single -> null, multiple -> array()
+ * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access.
+ * @param string $property The property name.
+ * @param string $single Whether to return just the first piece of meta data with the given property key, or all meta data.
+ * @param mixed $default (optional) The value to return if no value is found - defaults to single -> null, multiple -> array().
+ *
* @since 2.2.0
* @return mixed
*/
function wcs_get_objects_property( $object, $property, $single = 'single', $default = null ) {
+ $prefixed_key = wcs_maybe_prefix_key( $property );
+ $value = ! is_null( $default ) ? $default : ( ( 'single' === $single ) ? null : array() );
+ $property_function_map = array(
+ 'order_version' => 'version',
+ 'order_currency' => 'currency',
+ 'order_date' => 'date_created',
+ 'date' => 'date_created',
+ 'cart_discount' => 'total_discount',
+ );
- $prefixed_key = wcs_maybe_prefix_key( $property );
-
- $value = ! is_null( $default ) ? $default : ( ( 'single' == $single ) ? null : array() );
+ if ( isset( $property_function_map[ $property ] ) ) {
+ $property = $property_function_map[ $property ];
+ }
switch ( $property ) {
-
- case 'name' : // the replacement for post_title added in 3.0
- if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
- $value = $object->post->post_title;
- } else { // WC 3.0+
- $value = $object->get_name();
- }
- break;
-
case 'post' :
- if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
- $value = $object->post;
- } else { // WC 3.0+
- // In order to keep backwards compatibility it's required to use the parent data for variations.
- if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) {
- $value = get_post( $object->get_parent_id() );
- } else {
- $value = get_post( $object->get_id() );
- }
+ // In order to keep backwards compatibility it's required to use the parent data for variations.
+ if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) {
+ $value = get_post( wcs_get_objects_property( $object, 'parent_id' ) );
+ } else {
+ $value = get_post( wcs_get_objects_property( $object, 'id' ) );
}
break;
@@ -94,113 +91,32 @@ function wcs_get_objects_property( $object, $property, $single = 'single', $defa
$value = wcs_get_objects_property( $object, 'post' )->post_status;
break;
- case 'parent_id' :
- if ( method_exists( $object, 'get_parent_id' ) ) { // WC 3.0+ or an instance of WC_Product_Subscription_Variation_Legacy with WC < 3.0
- $value = $object->get_parent_id();
- } else { // WC 2.1-2.6
- $value = $object->get_parent();
- }
- break;
-
case 'variation_data' :
- if ( function_exists( 'wc_get_product_variation_attributes' ) ) { // WC 3.0+
- $value = wc_get_product_variation_attributes( $object->get_id() );
- } else {
- $value = $object->$property;
- }
- break;
-
- case 'downloads' :
- if ( method_exists( $object, 'get_downloads' ) ) { // WC 3.0+
- $value = $object->get_downloads();
- } else {
- $value = $object->get_files();
- }
- break;
-
- case 'order_version' :
- case 'version' :
- if ( method_exists( $object, 'get_version' ) ) { // WC 3.0+
- $value = $object->get_version();
- } else { // WC 2.1-2.6
- $value = $object->order_version;
- }
- break;
-
- case 'order_currency' :
- case 'currency' :
- if ( method_exists( $object, 'get_currency' ) ) { // WC 3.0+
- $value = $object->get_currency();
- } else { // WC 2.1-2.6
- $value = $object->get_order_currency();
- }
- break;
-
- // Always return a PHP DateTime object in site timezone (or null), the same thing the WC_Order::get_date_created() method returns in WC 3.0+ to make it easier to migrate away from WC < 3.0
- case 'date_created' :
- case 'order_date' :
- case 'date' :
- if ( method_exists( $object, 'get_date_created' ) ) { // WC 3.0+
- $value = $object->get_date_created();
- } else {
- // Base the value off tht GMT value when possible and then set the DateTime's timezone based on the current site's timezone to avoid incorrect values when the timezone has changed
- if ( '0000-00-00 00:00:00' != $object->post->post_date_gmt ) {
- $value = new WC_DateTime( $object->post->post_date_gmt, new DateTimeZone( 'UTC' ) );
- $value->setTimezone( new DateTimeZone( wc_timezone_string() ) );
- } else {
- $value = new WC_DateTime( $object->post->post_date, new DateTimeZone( wc_timezone_string() ) );
- }
- }
- break;
-
- // Always return a PHP DateTime object in site timezone (or null), the same thing the getter returns in WC 3.0+ to make it easier to migrate away from WC < 3.0
- case 'date_paid' :
- if ( method_exists( $object, 'get_date_paid' ) ) { // WC 3.0+
- $value = $object->get_date_paid();
- } else {
- if ( ! empty( $object->paid_date ) ) {
- // Because the paid_date post meta value was set in the site timezone at the time it was set, this won't always be correct, but is the best we can do with WC < 3.0
- $value = new WC_DateTime( $object->paid_date, new DateTimeZone( wc_timezone_string() ) );
- } else {
- $value = null;
- }
- }
- break;
-
- case 'cart_discount' :
- if ( method_exists( $object, 'get_total_discount' ) ) { // WC 3.0+
- $value = $object->get_total_discount();
- } else { // WC 2.1-2.6
- $value = $object->cart_discount;
- }
+ $value = wc_get_product_variation_attributes( wcs_get_objects_property( $object, 'id' ) );
break;
default :
-
$function_name = 'get_' . $property;
if ( is_callable( array( $object, $function_name ) ) ) {
$value = $object->$function_name();
} else {
-
- // If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that
- if ( method_exists( $object, 'get_meta' ) ) {
- if ( $object->meta_exists( $prefixed_key ) ) {
- if ( 'single' === $single ) {
- $value = $object->get_meta( $prefixed_key, true );
- } else {
- // WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available
- $value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' );
- }
+ // If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that.
+ if ( $object->meta_exists( $prefixed_key ) ) {
+ if ( 'single' === $single ) {
+ $value = $object->get_meta( $prefixed_key, true );
+ } else {
+ // WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available.
+ $value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' );
}
- } elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0
+ } elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0.
$value = $object->$property;
} elseif ( strtolower( $property ) !== 'id' && metadata_exists( 'post', wcs_get_objects_property( $object, 'id' ), $prefixed_key ) ) {
- // If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did
+ // If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did.
if ( 'single' === $single ) {
$value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, true );
} else {
- // Get all the meta values
+ // Get all the meta values.
$value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, false );
}
}
@@ -519,3 +435,48 @@ function wcs_set_coupon_property( &$coupon, $property, $value ) {
}
}
}
+
+/**
+ * Generate an order/subscription key.
+ *
+ * This is a compatibility wrapper for @see wc_generate_order_key() which was introduced in WC 3.5.4.
+ *
+ * @return string $order_key.
+ * @since 2.5.0
+ */
+function wcs_generate_order_key() {
+
+ if ( function_exists( 'wc_generate_order_key' ) ) {
+ $order_key = wc_generate_order_key();
+ } else {
+ $order_key = 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . wp_generate_password( 13, false ) );
+ }
+
+ return $order_key;
+}
+
+/**
+ * Update a single option for a WC_Settings_API object.
+ *
+ * This is a compatibility wrapper for @see WC_Settings_API::update_option() which was introduced in WC 3.4.0.
+ *
+ * @param WC_Settings_API $settings_api The object to update the option for.
+ * @param string $key Option key.
+ * @param mixed $value Value to set.
+ * @since 2.5.1
+ */
+function wcs_update_settings_option( $settings_api, $key, $value ) {
+
+ // WooCommerce 3.4+
+ if ( is_callable( array( $settings_api, 'update_option' ) ) ) {
+ $settings_api->update_option( $key, $value );
+ } else {
+ if ( empty( $settings_api->settings ) ) {
+ $settings_api->init_settings();
+ }
+
+ $settings_api->settings[ $key ] = $value;
+
+ return update_option( $settings_api->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $settings_api->id, $settings_api->settings ), 'yes' );
+ }
+}
diff --git a/includes/wcs-formatting-functions.php b/includes/wcs-formatting-functions.php
index ecf3bd6..560fc6c 100755
--- a/includes/wcs-formatting-functions.php
+++ b/includes/wcs-formatting-functions.php
@@ -157,7 +157,7 @@ function wcs_price_string( $subscription_details ) {
if ( 1 == $subscription_details['subscription_interval'] ) {
// e.g. $15 on March 15th each year
if ( ! empty( $subscription_details['initial_amount'] ) ) {
- // translators: 1$: initial amount, 2$: intial description (e.g. "up front"), 3$: recurring amount, 4$: month of year (e.g. "March"), 5$: day of the month (e.g. "23rd")
+ // translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: month of year (e.g. "March"), 5$: day of the month (e.g. "23rd")
$subscription_string = sprintf( __( '%1$s %2$s then %3$s on %4$s %5$s each year', 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $wp_locale->month[ $payment_day['month'] ], WC_Subscriptions::append_numeral_suffix( $payment_day['day'] ) );
} else {
// translators: 1$: recurring amount, 2$: month (e.g. "March"), 3$: day of the month (e.g. "23rd") (e.g. "$15 on March 15th every 3rd year")
@@ -213,7 +213,7 @@ function wcs_price_string( $subscription_details ) {
}
/**
- * Display a human friendly time diff for a given timestamp, e.g. "In 12 hours" or "12 hours ago".
+ * Display a human friendly time diff for a given timestamp, e.g. "in 12 hours" or "12 hours ago".
*
* @param int $timestamp_gmt
* @return string A human friendly string to display for the timestamp's date
@@ -225,13 +225,15 @@ function wcs_get_human_time_diff( $timestamp_gmt ) {
if ( $time_diff > 0 && $time_diff < WEEK_IN_SECONDS ) {
// translators: placeholder is human time diff (e.g. "3 weeks")
- $date_to_display = sprintf( __( 'In %s', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) );
+ $date_to_display = sprintf( __( 'in %s', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) );
} elseif ( $time_diff < 0 && absint( $time_diff ) < WEEK_IN_SECONDS ) {
// translators: placeholder is human time diff (e.g. "3 weeks")
$date_to_display = sprintf( __( '%s ago', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) );
} else {
$timestamp_site = wcs_date_to_time( get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $timestamp_gmt ) ) );
$date_to_display = date_i18n( wc_date_format(), $timestamp_site ) . ' ' . date_i18n( wc_time_format(), $timestamp_site );
+ // translators: placeholder is a localized date and time (e.g. "February 1, 2018 10:20 PM")
+ $date_to_display = sprintf( _x( '%s', 'wcs_get_human_time_diff', 'woocommerce-subscriptions' ), $date_to_display );
}
return $date_to_display;
diff --git a/includes/wcs-order-functions.php b/includes/wcs-order-functions.php
index e4c4259..8dac055 100755
--- a/includes/wcs-order-functions.php
+++ b/includes/wcs-order-functions.php
@@ -747,6 +747,8 @@ function wcs_display_item_meta( $item, $order ) {
* @return void
*/
function wcs_display_item_downloads( $item, $order ) {
+ wcs_deprecated_function( __FUNCTION__, '2.5.0', 'wc_display_item_downloads( $item )' );
+
if ( function_exists( 'wc_display_item_downloads' ) ) { // WC 3.0+
wc_display_item_downloads( $item );
} else {
@@ -865,4 +867,32 @@ function wcs_copy_payment_method_to_order( $subscription, $order ) {
if ( ! empty( $payment_meta ) ) {
wcs_set_payment_meta( $order, $payment_meta );
}
+
+}
+
+/**
+ * Returns how many minutes ago the order was created.
+ *
+ * @param WC_Order $order
+ *
+ * @return int
+ * @since 2.5.3
+ */
+function wcs_minutes_since_order_created( $order ) {
+ $now = new WC_DateTime( 'now', $order->get_date_created()->getTimezone() );
+ $diff_in_minutes = $now->diff( $order->get_date_created() );
+
+ return absint( $diff_in_minutes->i );
+}
+
+/**
+ * Returns how many seconds ago the order was created.
+ *
+ * @param WC_Order $order
+ *
+ * @return int
+ * @since 2.5.3
+ */
+function wcs_seconds_since_order_created( $order ) {
+ return time() - $order->get_date_created()->getTimestamp();
}
diff --git a/includes/wcs-user-functions.php b/includes/wcs-user-functions.php
index 2cd8892..d2bb476 100755
--- a/includes/wcs-user-functions.php
+++ b/includes/wcs-user-functions.php
@@ -334,7 +334,7 @@ function wcs_get_all_user_actions_for_subscription( $subscription, $user_id ) {
);
}
- if ( wcs_can_user_resubscribe_to( $subscription, $user_id ) ) {
+ if ( wcs_can_user_resubscribe_to( $subscription, $user_id ) && false == $subscription->can_be_updated_to( 'active' ) ) {
$actions['resubscribe'] = array(
'url' => wcs_get_users_resubscribe_link( $subscription ),
'name' => __( 'Resubscribe', 'woocommerce-subscriptions' ),
diff --git a/languages/woocommerce-subscriptions.pot b/languages/woocommerce-subscriptions.pot
index b5cd118..9087956 100755
--- a/languages/woocommerce-subscriptions.pot
+++ b/languages/woocommerce-subscriptions.pot
@@ -1,92 +1,100 @@
-# Copyright (C) 2018 Prospress Inc.
+# Copyright (C) 2019 Prospress Inc.
# This file is distributed under the same license as the WooCommerce Subscriptions package.
msgid ""
msgstr ""
-"Project-Id-Version: WooCommerce Subscriptions 2.4.7\n"
+"Project-Id-Version: WooCommerce Subscriptions 2.5.3\n"
"Report-Msgid-Bugs-To: "
"https://github.com/Prospress/woocommerce-subscriptions/issues\n"
-"POT-Creation-Date: 2018-12-21 11:20:12+00:00\n"
+"POT-Creation-Date: 2019-03-20 06:30:32+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2018-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: 2019-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: Prospress Translations \n"
"X-Generator: grunt-wp-i18n 0.5.4\n"
"Language: en_US\n"
-#: includes/abstracts/abstract-wcs-related-order-store.php:138
+#: includes/abstracts/abstract-wcs-related-order-store.php:147
msgid "Invalid relation type: %s. Order relationship type must be one of: %s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:185
+#: includes/admin/class-wc-subscriptions-admin.php:191
msgid "Simple subscription"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:186
+#: includes/admin/class-wc-subscriptions-admin.php:192
msgid "Variable subscription"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:204
+#: includes/admin/class-wc-subscriptions-admin.php:213
+msgid "Downloadable"
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:214
+msgid "Virtual"
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:278
msgid "Choose the subscription price, billing interval and period."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:217
+#: includes/admin/class-wc-subscriptions-admin.php:291
#: templates/admin/html-variation-price.php:44
#. translators: placeholder is a currency symbol / code
msgid "Subscription price (%s)"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:220
+#: includes/admin/class-wc-subscriptions-admin.php:294
msgid "Subscription interval"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:226
-#: includes/admin/class-wc-subscriptions-admin.php:362
+#: includes/admin/class-wc-subscriptions-admin.php:300
+#: includes/admin/class-wc-subscriptions-admin.php:437
msgid "Subscription period"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:240
-#: includes/admin/class-wc-subscriptions-admin.php:363
+#: includes/admin/class-wc-subscriptions-admin.php:314
+#: includes/admin/class-wc-subscriptions-admin.php:438
#: templates/admin/html-variation-price.php:66
msgid "Expire after"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:243
+#: includes/admin/class-wc-subscriptions-admin.php:317
msgid ""
"Automatically expire the subscription after this length of time. This "
"length is in addition to any free trial or amount of time provided before a "
"synchronised first renewal date."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:252
+#: includes/admin/class-wc-subscriptions-admin.php:327
#: templates/admin/html-variation-price.php:20
#. translators: %s is a currency symbol / code
msgid "Sign-up fee (%s)"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:254
+#: includes/admin/class-wc-subscriptions-admin.php:329
msgid ""
"Optionally include an amount to be charged at the outset of the "
"subscription. The sign-up fee will be charged immediately, even if the "
"product has a free trial or the payment dates are synced."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:266
+#: includes/admin/class-wc-subscriptions-admin.php:341
#: templates/admin/html-variation-price.php:25
msgid "Free trial"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:269
+#: includes/admin/class-wc-subscriptions-admin.php:344
#: templates/admin/deprecated/html-variation-price.php:115
msgid "Subscription Trial Period"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:301
+#: includes/admin/class-wc-subscriptions-admin.php:376
msgid "One time shipping"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:302
+#: includes/admin/class-wc-subscriptions-admin.php:377
msgid ""
"Shipping for subscription products is normally charged on the initial order "
"and all renewal orders. Enable this to only charge shipping once on the "
@@ -94,63 +102,63 @@ msgid ""
"not have a free trial or a synced renewal date."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:359
+#: includes/admin/class-wc-subscriptions-admin.php:434
msgid "Subscription pricing"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:360
+#: includes/admin/class-wc-subscriptions-admin.php:435
msgid "Subscription sign-up fee"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:361
+#: includes/admin/class-wc-subscriptions-admin.php:436
msgid "Subscription billing interval"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:364
+#: includes/admin/class-wc-subscriptions-admin.php:439
msgid "Free trial length"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:365
+#: includes/admin/class-wc-subscriptions-admin.php:440
msgid "Free trial period"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:686
+#: includes/admin/class-wc-subscriptions-admin.php:764
msgid ""
"Unable to change subscription status to \"%s\". Please assign a customer to "
"the subscription to activate it."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:728
+#: includes/admin/class-wc-subscriptions-admin.php:806
msgid ""
"Trashing this order will also trash the subscriptions purchased with the "
"order."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:741
+#: includes/admin/class-wc-subscriptions-admin.php:819
msgid "Enter the new period, either day, week, month or year:"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:742
+#: includes/admin/class-wc-subscriptions-admin.php:820
msgid "Enter a new length (e.g. 5):"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:743
+#: includes/admin/class-wc-subscriptions-admin.php:821
msgid ""
"Enter a new interval as a single number (e.g. to charge every 2nd month, "
"enter 2):"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:744
+#: includes/admin/class-wc-subscriptions-admin.php:822
msgid "Delete all variations without a subscription"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:747
+#: includes/admin/class-wc-subscriptions-admin.php:825
msgid ""
"Product type can not be changed because this product is associated with "
"active subscriptions"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:751
+#: includes/admin/class-wc-subscriptions-admin.php:829
msgid ""
"You are about to trash one or more orders which contain a subscription.\n"
"\n"
@@ -158,7 +166,7 @@ msgid ""
"orders."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:764
+#: includes/admin/class-wc-subscriptions-admin.php:842
msgid ""
"WARNING: Bad things are about to happen!\n"
"\n"
@@ -170,13 +178,13 @@ msgid ""
"gateway."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:765
+#: includes/admin/class-wc-subscriptions-admin.php:843
msgid ""
"You are deleting a subscription item. You will also need to manually cancel "
"and trash the subscription on the Manage Subscriptions screen."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:772
+#: includes/admin/class-wc-subscriptions-admin.php:850
msgid ""
"Warning: Deleting a user will also delete the user's subscriptions. The "
"user's orders will remain but be reassigned to the 'Guest' user.\n"
@@ -185,63 +193,75 @@ msgid ""
"subscriptions?"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:835
+#: includes/admin/class-wc-subscriptions-admin.php:854
+msgid ""
+"PayPal Standard has a number of limitations and does not support all "
+"subscription features."
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:854
+msgid ""
+"Because of this, it is not recommended as a payment method for "
+"Subscriptions unless it is the only available option for your country."
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:917
msgid "Active subscriber?"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:878
+#: includes/admin/class-wc-subscriptions-admin.php:960
msgid "Manage Subscriptions"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:882
-#: woocommerce-subscriptions.php:262
+#: includes/admin/class-wc-subscriptions-admin.php:964
+#: woocommerce-subscriptions.php:263
msgid "Search Subscriptions"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:902
-#: includes/admin/class-wc-subscriptions-admin.php:1017
+#: includes/admin/class-wc-subscriptions-admin.php:984
+#: includes/admin/class-wc-subscriptions-admin.php:1099
#: includes/admin/class-wcs-admin-reports.php:46
#: includes/admin/class-wcs-admin-system-status.php:56
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:688
-#: includes/class-wcs-query.php:108 includes/class-wcs-query.php:129
-#: includes/class-wcs-query.php:248
+#: includes/class-wcs-query.php:115 includes/class-wcs-query.php:142
+#: includes/class-wcs-query.php:296
#: includes/privacy/class-wcs-privacy-exporters.php:51
-#: woocommerce-subscriptions.php:253 woocommerce-subscriptions.php:266
+#: woocommerce-subscriptions.php:254 woocommerce-subscriptions.php:267
msgid "Subscriptions"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1057
+#: includes/admin/class-wc-subscriptions-admin.php:1139
msgid "Button Text"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1064
+#: includes/admin/class-wc-subscriptions-admin.php:1146
msgid "Add to Cart Button Text"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1065
+#: includes/admin/class-wc-subscriptions-admin.php:1147
msgid ""
"A product displays a button with the text \"Add to Cart\". By default, a "
"subscription changes this to \"Sign Up Now\". You can customise the button "
"text for subscriptions here."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1069
-#: includes/admin/class-wc-subscriptions-admin.php:1072
-#: includes/admin/class-wc-subscriptions-admin.php:1081
-#: includes/admin/class-wc-subscriptions-admin.php:1084
+#: includes/admin/class-wc-subscriptions-admin.php:1151
+#: includes/admin/class-wc-subscriptions-admin.php:1154
+#: includes/admin/class-wc-subscriptions-admin.php:1163
+#: includes/admin/class-wc-subscriptions-admin.php:1166
#: includes/class-wc-product-subscription-variation.php:98
#: includes/class-wc-product-subscription.php:72
#: includes/class-wc-product-variable-subscription.php:73
#: includes/class-wc-subscriptions-product.php:99
-#: woocommerce-subscriptions.php:560
+#: woocommerce-subscriptions.php:578
msgid "Sign Up Now"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1076
+#: includes/admin/class-wc-subscriptions-admin.php:1158
msgid "Place Order Button Text"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1077
+#: includes/admin/class-wc-subscriptions-admin.php:1159
msgid ""
"Use this field to customise the text displayed on the checkout button when "
"an order contains a subscription. Normally the checkout submission button "
@@ -249,11 +269,11 @@ msgid ""
"changed to \"Sign Up Now\"."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1090
+#: includes/admin/class-wc-subscriptions-admin.php:1172
msgid "Roles"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1093
+#: includes/admin/class-wc-subscriptions-admin.php:1175
#. translators: placeholders are tags
msgid ""
"Choose the default roles to assign to active and inactive subscribers. For "
@@ -262,46 +282,46 @@ msgid ""
"allocated these roles to prevent locking out administrators."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1098
+#: includes/admin/class-wc-subscriptions-admin.php:1180
msgid "Subscriber Default Role"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1099
+#: includes/admin/class-wc-subscriptions-admin.php:1181
msgid ""
"When a subscription is activated, either manually or after a successful "
"purchase, new users will be assigned this role."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1110
+#: includes/admin/class-wc-subscriptions-admin.php:1192
msgid "Inactive Subscriber Role"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1111
+#: includes/admin/class-wc-subscriptions-admin.php:1193
msgid ""
"If a subscriber's subscription is manually cancelled or expires, she will "
"be assigned this role."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1131
+#: includes/admin/class-wc-subscriptions-admin.php:1213
msgid "Manual Renewal Payments"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1132
+#: includes/admin/class-wc-subscriptions-admin.php:1214
msgid "Accept Manual Renewals"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1137
+#: includes/admin/class-wc-subscriptions-admin.php:1219
#. translators: placeholders are opening and closing link tags
msgid ""
"With manual renewals, a customer's subscription is put on-hold until they "
"login and pay to renew it. %sLearn more%s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1143
+#: includes/admin/class-wc-subscriptions-admin.php:1225
msgid "Turn off Automatic Payments"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1148
+#: includes/admin/class-wc-subscriptions-admin.php:1230
#. translators: placeholders are opening and closing link tags
msgid ""
"If you don't want new subscription purchases to automatically charge "
@@ -310,11 +330,11 @@ msgid ""
"more%s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1163
+#: includes/admin/class-wc-subscriptions-admin.php:1245
msgid "Customer Suspensions"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1170
+#: includes/admin/class-wc-subscriptions-admin.php:1252
msgid ""
"Set a maximum number of times a customer can suspend their account for each "
"billing period. For example, for a value of 3 and a subscription billed "
@@ -324,30 +344,46 @@ msgid ""
"this to 0 to turn off the customer suspension feature completely."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1174
+#: includes/admin/class-wc-subscriptions-admin.php:1256
msgid "Mixed Checkout"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1175
+#: includes/admin/class-wc-subscriptions-admin.php:1257
msgid "Allow multiple subscriptions and products to be purchased simultaneously."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1179
+#: includes/admin/class-wc-subscriptions-admin.php:1261
msgid ""
"Allow a subscription product to be purchased with other products and "
"subscriptions in the same transaction."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1183
+#: includes/admin/class-wc-subscriptions-admin.php:1265
+msgid "$0 Initial Checkout"
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:1266
+msgid "Allow $0 initial checkout without a payment method."
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:1270
+msgid ""
+"Allow a subscription product with a $0 initial payment to be purchased "
+"without providing a payment method. The customer will be required to "
+"provide a payment method at the end of the initial period to keep the "
+"subscription active."
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:1274
#: includes/upgrades/templates/wcs-about-2-0.php:108
msgid "Drip Downloadable Content"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1184
+#: includes/admin/class-wc-subscriptions-admin.php:1275
msgid "Enable dripping for downloadable content on subscription products."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1188
+#: includes/admin/class-wc-subscriptions-admin.php:1279
msgid ""
"Enabling this grants access to new downloadable files added to a product "
"only after the next renewal is processed.%sBy default, access to new "
@@ -355,7 +391,7 @@ msgid ""
"customer that has an active subscription with that product."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1224
+#: includes/admin/class-wc-subscriptions-admin.php:1315
#. translators: $1-$2: opening and closing tags, $3-$4: opening and
#. closing tags
msgid ""
@@ -363,73 +399,77 @@ msgid ""
"start selling subscriptions!%4$s"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1229
+#: includes/admin/class-wc-subscriptions-admin.php:1320
msgid "Add a Subscription Product"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1230
+#: includes/admin/class-wc-subscriptions-admin.php:1321
#: includes/upgrades/templates/wcs-about-2-0.php:35
#: includes/upgrades/templates/wcs-about.php:34
-#: woocommerce-subscriptions.php:1091
+#: woocommerce-subscriptions.php:1110
msgid "Settings"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1316
+#: includes/admin/class-wc-subscriptions-admin.php:1405
#. translators: placeholder is a number
msgid "We can't find a subscription with ID #%d. Perhaps it was deleted?"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1349
-#: includes/admin/class-wc-subscriptions-admin.php:1354
+#: includes/admin/class-wc-subscriptions-admin.php:1445
+msgid "We can't find a paid subscription order for this user."
+msgstr ""
+
+#: includes/admin/class-wc-subscriptions-admin.php:1477
+#: includes/admin/class-wc-subscriptions-admin.php:1482
#. translators: placeholders are opening link tag, ID of sub, and closing link
#. tag
msgid "Showing orders for %sSubscription %s%s"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1378
+#: includes/admin/class-wc-subscriptions-admin.php:1506
#. translators: number of 1$: days, 2$: weeks, 3$: months, 4$: years
msgid "The trial period can not exceed: %1s, %2s, %3s or %4s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1383
+#: includes/admin/class-wc-subscriptions-admin.php:1511
#. translators: placeholder is a time period (e.g. "4 weeks")
msgid "The trial period can not exceed %s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1446
-#: includes/admin/class-wc-subscriptions-admin.php:1499
+#: includes/admin/class-wc-subscriptions-admin.php:1574
+#: includes/admin/class-wc-subscriptions-admin.php:1641
#: includes/admin/class-wcs-admin-system-status.php:95
-#: includes/admin/reports/class-wcs-report-cache-manager.php:328
+#: includes/admin/reports/class-wcs-report-cache-manager.php:331
msgid "Yes"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1446
+#: includes/admin/class-wc-subscriptions-admin.php:1574
#: includes/admin/class-wcs-admin-system-status.php:95
-#: includes/admin/reports/class-wcs-report-cache-manager.php:328
+#: includes/admin/reports/class-wcs-report-cache-manager.php:331
msgid "No"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1482
+#: includes/admin/class-wc-subscriptions-admin.php:1610
msgid "Automatic Recurring Payments"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1499
+#: includes/admin/class-wc-subscriptions-admin.php:1641
msgid ""
"Supports automatic renewal payments with the WooCommerce Subscriptions "
"extension."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1579
+#: includes/admin/class-wc-subscriptions-admin.php:1736
msgid "Subscription items can no longer be edited."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1583
+#: includes/admin/class-wc-subscriptions-admin.php:1740
msgid ""
"This subscription is no longer editable because the payment gateway does "
"not allow modification of recurring amounts."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1602
+#: includes/admin/class-wc-subscriptions-admin.php:1759
#. translators: $1-2: opening and closing tags of a link that takes to Woo
#. marketplace / Stripe product page
msgid ""
@@ -438,18 +478,18 @@ msgid ""
"the %1$sfree Stripe extension%2$s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1607
+#: includes/admin/class-wc-subscriptions-admin.php:1764
msgid "Recurring Payments"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1615
+#: includes/admin/class-wc-subscriptions-admin.php:1772
#. translators: placeholders are opening and closing link tags
msgid ""
"Payment gateways which don't support automatic recurring payments can be "
"used to process %smanual subscription renewal payments%s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1622
+#: includes/admin/class-wc-subscriptions-admin.php:1779
#. translators: $1-$2: opening and closing tags. Link to documents->payment
#. gateways, 3$-4$: opening and closing tags. Link to WooCommerce extensions
#. shop page
@@ -458,7 +498,7 @@ msgid ""
"the official %3$sWooCommerce Marketplace%4$s."
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:1724
+#: includes/admin/class-wc-subscriptions-admin.php:1881
msgid "Note that purchasing a subscription still requires an account."
msgstr ""
@@ -561,10 +601,10 @@ msgstr[1] ""
#: templates/myaccount/my-subscriptions.php:26
#: templates/myaccount/my-subscriptions.php:41
#: templates/myaccount/related-orders.php:24
-#: templates/myaccount/related-orders.php:45
+#: templates/myaccount/related-orders.php:50
#: templates/myaccount/related-subscriptions.php:21
#: templates/myaccount/related-subscriptions.php:35
-#: templates/myaccount/subscription-details.php:18
+#: templates/myaccount/subscription-details.php:17
msgid "Status"
msgstr ""
@@ -576,7 +616,7 @@ msgstr ""
#: templates/emails/subscription-info.php:18
#: templates/myaccount/my-subscriptions.php:25
#: templates/myaccount/related-subscriptions.php:20
-#: woocommerce-subscriptions.php:254
+#: woocommerce-subscriptions.php:255
msgid "Subscription"
msgstr ""
@@ -629,12 +669,12 @@ msgid "Delete Permanently"
msgstr ""
#: includes/admin/class-wcs-admin-post-types.php:487
-#: includes/class-wc-subscriptions-product.php:746
+#: includes/class-wc-subscriptions-product.php:748
msgid "Restore this item from the Trash"
msgstr ""
#: includes/admin/class-wcs-admin-post-types.php:487
-#: includes/class-wc-subscriptions-product.php:747
+#: includes/class-wc-subscriptions-product.php:749
msgid "Restore"
msgstr ""
@@ -671,7 +711,7 @@ msgstr[0] ""
msgstr[1] ""
#: includes/admin/class-wcs-admin-post-types.php:601
-#: templates/myaccount/my-subscriptions.php:49
+#: includes/class-wc-subscription.php:1949
#. translators: placeholder is the display name of a payment gateway a
#. subscription was paid by
msgid "Via %s"
@@ -687,51 +727,57 @@ msgid ""
"this subscription controls when payments are processed."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:884
-#: includes/admin/class-wcs-admin-post-types.php:887
-#: includes/admin/class-wcs-admin-post-types.php:890
+#: includes/admin/class-wcs-admin-post-types.php:893
+#: includes/admin/class-wcs-admin-post-types.php:896
+#: includes/admin/class-wcs-admin-post-types.php:899
msgid "Subscription updated."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:885
+#: includes/admin/class-wcs-admin-post-types.php:894
msgid "Custom field updated."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:886
+#: includes/admin/class-wcs-admin-post-types.php:895
msgid "Custom field deleted."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:891
+#: includes/admin/class-wcs-admin-post-types.php:900
msgid "Subscription saved."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:892
+#: includes/admin/class-wcs-admin-post-types.php:901
msgid "Subscription submitted."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:894
+#: includes/admin/class-wcs-admin-post-types.php:903
#. translators: php date string
msgid "Subscription scheduled for: %1$s."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:895
+#: includes/admin/class-wcs-admin-post-types.php:904
msgid "Subscription draft updated."
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:931
+#: includes/admin/class-wcs-admin-post-types.php:940
msgid "Any Payment Method"
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:932
+#: includes/admin/class-wcs-admin-post-types.php:941
msgid "None"
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:1125
+#: includes/admin/class-wcs-admin-post-types.php:947
+#: includes/class-wc-subscription.php:1932
+#: includes/class-wcs-change-payment-method-admin.php:155
+msgid "Manual Renewal"
+msgstr ""
+
+#: includes/admin/class-wcs-admin-post-types.php:1136
#. translators: 1: user display name 2: user ID 3: user email
msgid "%1$s (#%2$s – %3$s)"
msgstr ""
-#: includes/admin/class-wcs-admin-post-types.php:1132
+#: includes/admin/class-wcs-admin-post-types.php:1143
#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:84
msgid "Search for a customer…"
msgstr ""
@@ -761,7 +807,7 @@ msgid "Failed Payment Retries"
msgstr ""
#: includes/admin/class-wcs-admin-reports.php:104
-#: includes/admin/reports/class-wcs-report-cache-manager.php:274
+#: includes/admin/reports/class-wcs-report-cache-manager.php:277
msgid "WooCommerce"
msgstr ""
@@ -839,7 +885,7 @@ msgid "Billing Details"
msgstr ""
#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:134
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:206
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:222
#: includes/payment-retry/class-wcs-retry-post-store.php:38
msgid "Edit"
msgstr ""
@@ -850,8 +896,8 @@ msgstr ""
#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:142
#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:144
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:215
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:217
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:231
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:233
msgid "Address"
msgstr ""
@@ -866,31 +912,39 @@ msgid "Payment Method"
msgstr ""
#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:205
-msgid "Shipping Details"
+msgid "Customer change payment method page →"
msgstr ""
#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:207
+msgid "Customer add payment method page →"
+msgstr ""
+
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:221
+msgid "Shipping Details"
+msgstr ""
+
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:223
msgid "Copy from billing"
msgstr ""
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:208
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:224
msgid "Load shipping address"
msgstr ""
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:217
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:233
msgid "No shipping address set."
msgstr ""
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:239
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:269
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:255
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:285
msgid "Customer Provided Note"
msgstr ""
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:270
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:286
msgid "Customer's notes about the order"
msgstr ""
-#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:358
+#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:373
#. translators: placeholder is error message from the payment gateway or
#. subscriptions when updating the status
msgid "Error updating some information: %s"
@@ -902,7 +956,7 @@ msgid "Unpublished"
msgstr ""
#: includes/admin/meta-boxes/views/html-related-orders-table.php:17
-#: templates/myaccount/related-orders.php:37
+#: templates/myaccount/related-orders.php:42
msgid "Order Number"
msgstr ""
@@ -912,10 +966,10 @@ msgstr ""
#: includes/admin/meta-boxes/views/html-related-orders-table.php:19
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:549
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:173
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:190
#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:204
#: templates/myaccount/related-orders.php:23
-#: templates/myaccount/related-orders.php:42
+#: templates/myaccount/related-orders.php:47
msgid "Date"
msgstr ""
@@ -986,18 +1040,18 @@ msgstr ""
msgid "Error: unable to find timezone of your browser."
msgstr ""
-#: includes/admin/reports/class-wcs-report-cache-manager.php:277
+#: includes/admin/reports/class-wcs-report-cache-manager.php:280
msgid ""
"Please note: data for this report is cached. The data displayed may be out "
"of date by up to 24 hours. The cache is updated each morning at 4am in your "
"site's timezone."
msgstr ""
-#: includes/admin/reports/class-wcs-report-cache-manager.php:332
+#: includes/admin/reports/class-wcs-report-cache-manager.php:335
msgid "Cache Update Failures"
msgstr ""
-#: includes/admin/reports/class-wcs-report-cache-manager.php:335
+#: includes/admin/reports/class-wcs-report-cache-manager.php:338
#. translators: %d refers to the number of times we have detected cache update
#. failures
msgid "%d failures"
@@ -1005,15 +1059,35 @@ msgid_plural "%d failure"
msgstr[0] ""
msgstr[1] ""
-#: includes/admin/reports/class-wcs-report-dashboard.php:78
-msgid "%s signup subscription signups this month"
-msgid_plural "%s signups subscription signups this month"
+#: includes/admin/reports/class-wcs-report-dashboard.php:212
+#. translators: 1$: count, 2$ and 3$ are opening and closing strong tags,
+#. respectively.
+msgid "%2$s%$1s signup%3$s subscription signups this month"
+msgid_plural "%2$s%1$s signups%3$s subscription signups this month"
msgstr[0] ""
msgstr[1] ""
-#: includes/admin/reports/class-wcs-report-dashboard.php:83
-msgid "%s renewal subscription renewals this month"
-msgid_plural "%s renewals subscription renewals this month"
+#: includes/admin/reports/class-wcs-report-dashboard.php:218
+msgid "%s signup revenue this month"
+msgstr ""
+
+#: includes/admin/reports/class-wcs-report-dashboard.php:225
+#. translators: 1$: count, 2$ and 3$ are opening and closing strong tags,
+#. respectively.
+msgid "%2$s%1$s renewal%3$s subscription renewals this month"
+msgid_plural "%2$s%1$s renewals%3$s subscription renewals this month"
+msgstr[0] ""
+msgstr[1] ""
+
+#: includes/admin/reports/class-wcs-report-dashboard.php:231
+msgid "%s renewal revenue this month"
+msgstr ""
+
+#: includes/admin/reports/class-wcs-report-dashboard.php:238
+#. translators: 1$: count, 2$ and 3$ are opening and closing strong tags,
+#. respectively.
+msgid "%2$s%1$s cancellation%3$s subscription cancellations this month"
+msgid_plural "%2$s%1$s cancellations%3$s subscription cancellations this month"
msgstr[0] ""
msgstr[1] ""
@@ -1042,22 +1116,50 @@ msgstr ""
msgid "Total Subscribers"
msgstr ""
+#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:44
+msgid ""
+"The number of unique customers with a subscription of any status other than "
+"pending or trashed."
+msgstr ""
+
#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:45
msgid "Active Subscriptions"
msgstr ""
+#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:45
+msgid ""
+"The total number of subscriptions with a status of active or pending "
+"cancellation."
+msgstr ""
+
#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:46
msgid "Total Subscriptions"
msgstr ""
+#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:46
+msgid ""
+"The total number of subscriptions with a status other than pending or "
+"trashed."
+msgstr ""
+
#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:47
msgid "Total Subscription Orders"
msgstr ""
+#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:47
+msgid ""
+"The total number of sign-up, switch and renewal orders placed with your "
+"store with a paid status (i.e. processing or complete)."
+msgstr ""
+
#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:48
msgid "Average Lifetime Value"
msgstr ""
+#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:48
+msgid "The average value of all customers' sign-up, switch and renewal orders."
+msgstr ""
+
#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:96
msgid "Active Subscriptions %s"
msgstr ""
@@ -1176,7 +1278,8 @@ msgstr ""
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:420
msgid ""
"The number of subscriptions created during this period, either by being "
-"manually created, imported or a customer placing an order."
+"manually created, imported or a customer placing an order. This includes "
+"orders pending payment."
msgstr ""
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:426
@@ -1260,27 +1363,27 @@ msgid "Change in subscriptions between the start and end of the period."
msgstr ""
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:506
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:137
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:154
msgid "Year"
msgstr ""
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:507
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:138
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:155
msgid "Last Month"
msgstr ""
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:508
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:139
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:156
msgid "This Month"
msgstr ""
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:509
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:140
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:157
msgid "Last 7 Days"
msgstr ""
#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:553
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:177
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:194
#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:208
msgid "Export CSV"
msgstr ""
@@ -1325,68 +1428,68 @@ msgstr ""
msgid "Renewal Totals"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:95
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:112
msgid "%s renewal revenue recovered"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:96
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:113
msgid ""
"The total amount of revenue, including tax and shipping, recovered with the "
"failed payment retry system for renewal orders with a failed payment."
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:102
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:119
#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:93
msgid "%s renewal orders"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:103
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:120
msgid ""
"The number of renewal orders which had a failed payment use the retry "
"system."
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:108
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:125
msgid "%s retry attempts succeeded"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:109
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:126
msgid ""
"The number of renewal payment retries for this period which were able to "
"process the payment which had previously failed one or more times."
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:115
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:132
msgid "%s retry attempts failed"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:116
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:133
msgid ""
"The number of renewal payment retries for this period which did not result "
"in a successful payment."
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:122
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:139
msgid "%s retry attempts pending"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:123
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:140
msgid "The number of renewal payment retries not yet processed."
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:225
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:242
msgid "Successful retries"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:241
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:258
msgid "Failed retries"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:257
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:274
msgid "Pending retries"
msgstr ""
-#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:273
+#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:290
msgid "Recovered Renewal Revenue"
msgstr ""
@@ -1548,75 +1651,78 @@ msgstr ""
msgid "Cannot create subscription: %s."
msgstr ""
-#: includes/class-wc-subscription.php:413
+#: includes/class-wc-subscription.php:415
msgid "Unable to change subscription status to \"%s\"."
msgstr ""
-#: includes/class-wc-subscription.php:515
+#: includes/class-wc-subscription.php:527
msgid "Unable to change subscription status to \"%s\". Exception: %s"
msgstr ""
-#: includes/class-wc-subscription.php:537
+#: includes/class-wc-subscription.php:557
#. translators: 1: old subscription status 2: new subscription status
msgid "Status changed from %1$s to %2$s."
msgstr ""
-#: includes/class-wc-subscription.php:549
+#: includes/class-wc-subscription.php:571
#. translators: %s: new order status
msgid "Status set to %s."
msgstr ""
-#: includes/class-wc-subscription.php:1100
+#: includes/class-wc-subscription.php:585
+msgid "Error during subscription status transition."
+msgstr ""
+
+#: includes/class-wc-subscription.php:1132
#: includes/class-wc-subscriptions-manager.php:2279
-#: includes/wcs-formatting-functions.php:228
#. translators: placeholder is human time diff (e.g. "3 weeks")
msgid "In %s"
msgstr ""
-#: includes/class-wc-subscription.php:1103
+#: includes/class-wc-subscription.php:1135
#: includes/wcs-formatting-functions.php:231
#. translators: placeholder is human time diff (e.g. "3 weeks")
msgid "%s ago"
msgstr ""
-#: includes/class-wc-subscription.php:1110
+#: includes/class-wc-subscription.php:1142
msgid "Not yet ended"
msgstr ""
-#: includes/class-wc-subscription.php:1113
+#: includes/class-wc-subscription.php:1145
msgid "Not cancelled"
msgstr ""
-#: includes/class-wc-subscription.php:1228
+#: includes/class-wc-subscription.php:1260
msgid "The creation date of a subscription can not be deleted, only updated."
msgstr ""
-#: includes/class-wc-subscription.php:1231
+#: includes/class-wc-subscription.php:1263
msgid "The start date of a subscription can not be deleted, only updated."
msgstr ""
-#: includes/class-wc-subscription.php:1235
+#: includes/class-wc-subscription.php:1267
msgid "The %s date of a subscription can not be deleted. You must delete the order."
msgstr ""
-#: includes/class-wc-subscription.php:1243
-#: includes/class-wc-subscription.php:2302
+#: includes/class-wc-subscription.php:1275
+#: includes/class-wc-subscription.php:2360
msgid "Subscription #%d: "
msgstr ""
-#: includes/class-wc-subscription.php:1650
+#: includes/class-wc-subscription.php:1682
msgid "Payment status marked complete."
msgstr ""
-#: includes/class-wc-subscription.php:1678
+#: includes/class-wc-subscription.php:1710
msgid "Payment failed."
msgstr ""
-#: includes/class-wc-subscription.php:1683
+#: includes/class-wc-subscription.php:1715
msgid "Subscription Cancelled: maximum number of failed payments reached."
msgstr ""
-#: includes/class-wc-subscription.php:1793
+#: includes/class-wc-subscription.php:1825
msgid ""
"The \"all\" value for $order_type parameter is deprecated. It was a "
"misnomer, as it did not return resubscribe orders. It was also inconsistent "
@@ -1626,50 +1732,45 @@ msgid ""
"resubscribe."
msgstr ""
-#: includes/class-wc-subscription.php:1895
-#: includes/class-wcs-change-payment-method-admin.php:155
-msgid "Manual Renewal"
-msgstr ""
-
-#: includes/class-wc-subscription.php:1974 wcs-functions.php:794
+#: includes/class-wc-subscription.php:2022 wcs-functions.php:794
msgid "Payment method meta must be an array."
msgstr ""
-#: includes/class-wc-subscription.php:2200
+#: includes/class-wc-subscription.php:2258
msgid "Invalid format. First parameter needs to be an array."
msgstr ""
-#: includes/class-wc-subscription.php:2204
+#: includes/class-wc-subscription.php:2262
msgid "Invalid data. First parameter was empty when passed to update_dates()."
msgstr ""
-#: includes/class-wc-subscription.php:2211
+#: includes/class-wc-subscription.php:2269
msgid ""
"Invalid data. First parameter has a date that is not in the registered date "
"types."
msgstr ""
-#: includes/class-wc-subscription.php:2275
+#: includes/class-wc-subscription.php:2333
msgid "The %s date must occur after the cancellation date."
msgstr ""
-#: includes/class-wc-subscription.php:2280
+#: includes/class-wc-subscription.php:2338
msgid "The %s date must occur after the last payment date."
msgstr ""
-#: includes/class-wc-subscription.php:2284
+#: includes/class-wc-subscription.php:2342
msgid "The %s date must occur after the next payment date."
msgstr ""
-#: includes/class-wc-subscription.php:2289
+#: includes/class-wc-subscription.php:2347
msgid "The %s date must occur after the trial end date."
msgstr ""
-#: includes/class-wc-subscription.php:2293
+#: includes/class-wc-subscription.php:2351
msgid "The %s date must occur after the start date."
msgstr ""
-#: includes/class-wc-subscription.php:2322
+#: includes/class-wc-subscription.php:2380
#: includes/class-wc-subscriptions-checkout.php:325
#: includes/wcs-order-functions.php:305
msgid "Backordered"
@@ -1691,93 +1792,109 @@ msgstr ""
msgid "Update the %1$s used for %2$sall%3$s of my active subscriptions"
msgstr ""
-#: includes/class-wc-subscriptions-cart.php:912
+#: includes/class-wc-subscriptions-cart.php:932
msgid "Please enter a valid postcode/ZIP."
msgstr ""
-#: includes/class-wc-subscriptions-cart.php:1083
+#: includes/class-wc-subscriptions-cart.php:1090
msgid ""
"That subscription product can not be added to your cart as it already "
"contains a subscription renewal."
msgstr ""
-#: includes/class-wc-subscriptions-cart.php:1171
+#: includes/class-wc-subscriptions-cart.php:1178
msgid "Invalid recurring shipping method."
msgstr ""
-#: includes/class-wc-subscriptions-cart.php:1994
+#: includes/class-wc-subscriptions-cart.php:2016
msgid "now"
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:175
+#: includes/class-wc-subscriptions-change-payment-gateway.php:179
#: templates/emails/plain/email-order-details.php:19
#. translators: placeholder is the subscription order number wrapped in
#. tags
msgid "Subscription Number: %s"
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:181
+#: includes/class-wc-subscriptions-change-payment-gateway.php:185
#. translators: placeholder is the subscription's next payment date (either
#. human readable or normal date) wrapped in tags
msgid "Next Payment Date: %s"
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:187
+#: includes/class-wc-subscriptions-change-payment-gateway.php:191
#. translators: placeholder is the formatted total to be paid for the
#. subscription wrapped in tags
msgid "Total: %s"
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:194
+#: includes/class-wc-subscriptions-change-payment-gateway.php:198
#. translators: placeholder is the display name of the payment method
msgid "Payment Method: %s"
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:206
+#: includes/class-wc-subscriptions-change-payment-gateway.php:210
msgid ""
"Sorry, this subscription change payment method request is invalid and "
"cannot be processed."
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:238
+#: includes/class-wc-subscriptions-change-payment-gateway.php:242
msgid "There was an error with your request. Please try again."
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:242
-#: templates/myaccount/view-subscription.php:20
+#: includes/class-wc-subscriptions-change-payment-gateway.php:246
+#: includes/class-wcs-template-loader.php:27
msgid "Invalid Subscription."
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:246
+#: includes/class-wc-subscriptions-change-payment-gateway.php:250
#: includes/class-wcs-cart-resubscribe.php:78
#: includes/class-wcs-cart-resubscribe.php:129
#: includes/class-wcs-user-change-status-handler.php:103
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:94
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:95
msgid "That doesn't appear to be one of your subscriptions."
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:250
+#: includes/class-wc-subscriptions-change-payment-gateway.php:254
+#: includes/class-wcs-query.php:252
msgid "The payment method can not be changed for that subscription."
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:256
+#: includes/class-wc-subscriptions-change-payment-gateway.php:260
#. translators: placeholder is next payment's date
msgid " Next payment is due %s."
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:262
+#: includes/class-wc-subscriptions-change-payment-gateway.php:266
#. translators: placeholder is either empty or "Next payment is due..."
msgid "Choose a new payment method.%s"
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:291
+#: includes/class-wc-subscriptions-change-payment-gateway.php:295
msgid "Invalid order."
msgstr ""
-#: includes/class-wc-subscriptions-change-payment-gateway.php:393
+#: includes/class-wc-subscriptions-change-payment-gateway.php:381
msgid "Payment method updated."
msgstr ""
+#: includes/class-wc-subscriptions-change-payment-gateway.php:381
+msgid "Payment method added."
+msgstr ""
+
+#: includes/class-wc-subscriptions-change-payment-gateway.php:421
+#: includes/class-wc-subscriptions-change-payment-gateway.php:423
+msgid "Payment method updated for all your current subscriptions."
+msgstr ""
+
+#: includes/class-wc-subscriptions-change-payment-gateway.php:832
+msgid ""
+"Please log in to your account below to choose a new payment method for your "
+"subscription."
+msgstr ""
+
#: includes/class-wc-subscriptions-checkout.php:185
#: includes/class-wc-subscriptions-checkout.php:356
#. translators: placeholder is an internal error number
@@ -1914,7 +2031,7 @@ msgid "Error: Unable to create renewal order with note \"%s\""
msgstr ""
#: includes/class-wc-subscriptions-manager.php:168
-#: includes/gateways/class-wc-subscriptions-payment-gateways.php:209
+#: includes/gateways/class-wc-subscriptions-payment-gateways.php:211
msgid "Subscription doesn't exist in scheduled action: %d"
msgstr ""
@@ -2135,7 +2252,7 @@ msgstr ""
msgid "%1$s and a %2$s sign-up fee"
msgstr ""
-#: includes/class-wc-subscriptions-product.php:947
+#: includes/class-wc-subscriptions-product.php:951
msgid ""
"This variation can not be removed because it is associated with active "
"subscriptions. To remove this variation, please cancel and delete the "
@@ -2471,7 +2588,7 @@ msgid "There was an error with your request to resubscribe. Please try again."
msgstr ""
#: includes/class-wcs-cart-resubscribe.php:74
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:90
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:91
msgid "That subscription does not exist. Has it been deleted?"
msgstr ""
@@ -2495,13 +2612,11 @@ msgid "Please choose a valid payment gateway to change to."
msgstr ""
#: includes/class-wcs-failed-scheduled-action-manager.php:134
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:178
-msgid "Ignore this error (not recommended)"
+msgid "Ignore this error"
msgstr ""
#: includes/class-wcs-failed-scheduled-action-manager.php:139
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:183
-msgid "Open a ticket"
+msgid "Learn more"
msgstr ""
#: includes/class-wcs-limiter.php:45
@@ -2527,14 +2642,28 @@ msgstr ""
msgid "Limit to one of any status"
msgstr ""
-#: includes/class-wcs-my-account-payment-methods.php:82
+#: includes/class-wcs-my-account-auto-renew-toggle.php:132
+msgid "Auto Renewal Toggle"
+msgstr ""
+
+#: includes/class-wcs-my-account-auto-renew-toggle.php:133
+msgid "Display the auto renewal toggle"
+msgstr ""
+
+#: includes/class-wcs-my-account-auto-renew-toggle.php:134
+msgid ""
+"Allow customers to turn on and off automatic renewals from their View "
+"Subscription page."
+msgstr ""
+
+#: includes/class-wcs-my-account-payment-methods.php:80
msgid ""
"The deleted payment method was used for automatic subscription payments, we "
"couldn't find an alternative token payment method token to change your "
"subscriptions to."
msgstr ""
-#: includes/class-wcs-my-account-payment-methods.php:106
+#: includes/class-wcs-my-account-payment-methods.php:102
#. translators: $1: the token/credit card label, 2$-3$: opening and closing
#. strong and link tags
msgid ""
@@ -2545,42 +2674,58 @@ msgid ""
"Subscriptions%3$s page."
msgstr ""
-#: includes/class-wcs-my-account-payment-methods.php:194
+#: includes/class-wcs-my-account-payment-methods.php:121
msgid "%s ending in %s"
msgstr ""
-#: includes/class-wcs-my-account-payment-methods.php:286
+#: includes/class-wcs-my-account-payment-methods.php:153
msgid ""
"Would you like to update your subscriptions to use this new payment method "
"- %1$s?%2$sYes%4$s | %3$sNo%4$s"
msgstr ""
+#: includes/class-wcs-permalink-manager.php:91
+#. translators: 1$-2$: opening and closing tags.
+msgid ""
+"Error saving Subscriptions endpoints: %1$sSubscriptions%2$s, %1$sView "
+"subscription%2$s and %1$sSubscription payment method%2$s cannot be the "
+"same. The changes have been reverted."
+msgstr ""
+
#: includes/class-wcs-post-meta-cache-manager.php:198
msgid ""
"Invalid update type: %s. Post update types supported are \"add\" or "
"\"delete\". Updates are done on post meta directly."
msgstr ""
-#: includes/class-wcs-query.php:106
+#: includes/class-wcs-query.php:113
msgid "Subscriptions (page %d)"
msgstr ""
-#: includes/class-wcs-query.php:127
+#: includes/class-wcs-query.php:140
msgid "My Subscription"
msgstr ""
-#: includes/class-wcs-query.php:249
+#: includes/class-wcs-query.php:297
msgid "Endpoint for the My Account → Subscriptions page"
msgstr ""
-#: includes/class-wcs-query.php:257
+#: includes/class-wcs-query.php:305
msgid "View subscription"
msgstr ""
-#: includes/class-wcs-query.php:258
+#: includes/class-wcs-query.php:306
msgid "Endpoint for the My Account → View Subscription page"
msgstr ""
+#: includes/class-wcs-query.php:314
+msgid "Subscription payment method"
+msgstr ""
+
+#: includes/class-wcs-query.php:315
+msgid "Endpoint for the My Account → Change Subscription Payment Method page"
+msgstr ""
+
#: includes/class-wcs-remove-item.php:108
msgid "Your request to undo your previous action was unsuccessful."
msgstr ""
@@ -2610,7 +2755,7 @@ msgid ""
"not support removing an item."
msgstr ""
-#: includes/class-wcs-retry-manager.php:316
+#: includes/class-wcs-retry-manager.php:347
msgid ""
"Payment retry attempted on renewal order with multiple related "
"subscriptions with no payment method in common."
@@ -2626,6 +2771,10 @@ msgstr ""
msgid "staging"
msgstr ""
+#: includes/class-wcs-template-loader.php:27
+msgid "My Account"
+msgstr ""
+
#: includes/class-wcs-user-change-status-handler.php:60
msgid ""
"You can not reactivate that subscription until paying to renew it. Please "
@@ -2713,42 +2862,41 @@ msgid ""
"related order queries are run."
msgstr ""
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:70
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:71
msgid "Renew Now"
msgstr ""
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:98
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:99
msgid ""
"You can not renew this subscription early. Please contact us if you need "
"assistance."
msgstr ""
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:104
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:105
msgid "Complete checkout to renew now."
msgstr ""
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:226
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:247
#. translators: placeholder contains a link to the order's edit screen.
msgid "Customer successfully renewed early with order %s."
msgstr ""
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:229
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:250
#. translators: placeholder contains a link to the order's edit screen.
msgid ""
"Failed to update subscription dates after customer renewed early with order "
"%s."
msgstr ""
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:317
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:338
msgid "Order %s created to record early renewal."
msgstr ""
-#: includes/early-renewal/class-wcs-cart-early-renewal.php:372
+#: includes/early-renewal/class-wcs-cart-early-renewal.php:393
msgid "Cancel"
msgstr ""
#: includes/early-renewal/class-wcs-early-renewal-manager.php:44
-#: includes/upgrades/class-wcs-upgrade-notice-manager.php:98
msgid "Early Renewal"
msgstr ""
@@ -2776,41 +2924,37 @@ msgstr ""
msgid "Subscription Cancelled"
msgstr ""
-#: includes/emails/class-wcs-email-cancelled-subscription.php:127
-#: includes/emails/class-wcs-email-customer-renewal-invoice.php:192
-#: includes/emails/class-wcs-email-expired-subscription.php:125
-#: includes/emails/class-wcs-email-on-hold-subscription.php:125
+#: includes/emails/class-wcs-email-cancelled-subscription.php:147
+#: includes/emails/class-wcs-email-customer-renewal-invoice.php:214
+#: includes/emails/class-wcs-email-expired-subscription.php:145
+#: includes/emails/class-wcs-email-on-hold-subscription.php:145
msgid "Enable this email notification"
msgstr ""
-#: includes/emails/class-wcs-email-cancelled-subscription.php:134
-#: includes/emails/class-wcs-email-expired-subscription.php:132
-#: includes/emails/class-wcs-email-on-hold-subscription.php:132
+#: includes/emails/class-wcs-email-cancelled-subscription.php:154
+#: includes/emails/class-wcs-email-expired-subscription.php:152
+#: includes/emails/class-wcs-email-on-hold-subscription.php:152
#. translators: placeholder is admin email
-msgid ""
-"Enter recipients (comma separated) for this email. Defaults to "
-"%s."
+msgid "Enter recipients (comma separated) for this email. Defaults to %s."
msgstr ""
-#: includes/emails/class-wcs-email-cancelled-subscription.php:141
-#: includes/emails/class-wcs-email-expired-subscription.php:139
-#: includes/emails/class-wcs-email-on-hold-subscription.php:139
+#: includes/emails/class-wcs-email-cancelled-subscription.php:161
+#: includes/emails/class-wcs-email-expired-subscription.php:159
+#: includes/emails/class-wcs-email-on-hold-subscription.php:159
msgid ""
"This controls the email subject line. Leave blank to use the default "
-"subject: %s."
+"subject: %s."
msgstr ""
-#: includes/emails/class-wcs-email-cancelled-subscription.php:148
-#: includes/emails/class-wcs-email-expired-subscription.php:146
-#: includes/emails/class-wcs-email-on-hold-subscription.php:146
+#: includes/emails/class-wcs-email-cancelled-subscription.php:168
msgid ""
"This controls the main heading contained within the email notification. "
-"Leave blank to use the default heading: %s."
+"Leave blank to use the default heading: %s."
msgstr ""
-#: includes/emails/class-wcs-email-cancelled-subscription.php:155
-#: includes/emails/class-wcs-email-expired-subscription.php:153
-#: includes/emails/class-wcs-email-on-hold-subscription.php:153
+#: includes/emails/class-wcs-email-cancelled-subscription.php:175
+#: includes/emails/class-wcs-email-expired-subscription.php:173
+#: includes/emails/class-wcs-email-on-hold-subscription.php:173
msgid "Choose which format of email to send."
msgstr ""
@@ -2907,11 +3051,18 @@ msgstr ""
msgid "Subscription Expired"
msgstr ""
-#: includes/emails/class-wcs-email-expired-subscription.php:58
-#: includes/emails/class-wcs-email-on-hold-subscription.php:58
+#: includes/emails/class-wcs-email-expired-subscription.php:78
+#: includes/emails/class-wcs-email-on-hold-subscription.php:78
msgid "Subscription argument passed in is not an object."
msgstr ""
+#: includes/emails/class-wcs-email-expired-subscription.php:166
+#: includes/emails/class-wcs-email-on-hold-subscription.php:166
+msgid ""
+"This controls the main heading contained within the email notification. "
+"Leave blank to use the default heading: %s."
+msgstr ""
+
#: includes/emails/class-wcs-email-new-renewal-order.php:22
msgid "New Renewal Order"
msgstr ""
@@ -2999,56 +3150,69 @@ msgstr ""
msgid "Your {blogname} renewal order receipt from {order_date}"
msgstr ""
-#: includes/gateways/class-wc-subscriptions-payment-gateways.php:131
+#: includes/gateways/class-wc-subscriptions-payment-gateways.php:133
msgid ""
"Sorry, it seems there are no available payment methods which support "
"subscriptions. Please contact us if you require assistance or wish to make "
"alternate arrangements."
msgstr ""
-#: includes/gateways/paypal/class-wcs-paypal.php:199
+#: includes/gateways/class-wc-subscriptions-payment-gateways.php:268
+msgid "Supported features:"
+msgstr ""
+
+#: includes/gateways/class-wc-subscriptions-payment-gateways.php:271
+msgid "Subscription features:"
+msgstr ""
+
+#: includes/gateways/class-wc-subscriptions-payment-gateways.php:275
+msgid "Change payment features:"
+msgstr ""
+
+#: includes/gateways/paypal/class-wcs-paypal.php:213
msgid "Unable to find order for PayPal billing agreement."
msgstr ""
-#: includes/gateways/paypal/class-wcs-paypal.php:256
+#: includes/gateways/paypal/class-wcs-paypal.php:275
msgid "An error occurred, please try again or try an alternate form of payment."
msgstr ""
-#: includes/gateways/paypal/class-wcs-paypal.php:360
+#: includes/gateways/paypal/class-wcs-paypal.php:379
#. translators: placeholders are PayPal API error code and PayPal API error
#. message
msgid "PayPal API error: (%d) %s"
msgstr ""
-#: includes/gateways/paypal/class-wcs-paypal.php:365
+#: includes/gateways/paypal/class-wcs-paypal.php:384
#. translators: placeholder is PayPal transaction status message
msgid "PayPal Transaction Held: %s"
msgstr ""
-#: includes/gateways/paypal/class-wcs-paypal.php:377
+#: includes/gateways/paypal/class-wcs-paypal.php:396
#. translators: placeholder is PayPal transaction status message
msgid "PayPal payment declined: %s"
msgstr ""
-#: includes/gateways/paypal/class-wcs-paypal.php:381
+#: includes/gateways/paypal/class-wcs-paypal.php:400
msgid "PayPal payment approved (ID: %s)"
msgstr ""
-#: includes/gateways/paypal/class-wcs-paypal.php:434
+#: includes/gateways/paypal/class-wcs-paypal.php:453
msgid ""
"Are you sure you want to change the payment method from PayPal standard?\n"
"\n"
"This will suspend the subscription at PayPal."
msgstr ""
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:59
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:63
+#. translators: $1 and $2 are opening and closing strong tags, respectively.
msgid ""
-"It is strongly recommended you do not change the Receiver Email "
-"address if you have active subscriptions with PayPal. Doing so can "
-"break existing subscriptions."
+"It is %sstrongly recommended you do not change the Receiver Email address%s "
+"if you have active subscriptions with PayPal. Doing so can break existing "
+"subscriptions."
msgstr ""
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:105
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:109
#. translators: placeholders are opening and closing link tags. 1$-2$: to docs
#. on woocommerce, 3$-4$ to gateway settings on the site
msgid ""
@@ -3057,7 +3221,7 @@ msgid ""
"Subscriptions."
msgstr ""
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:116
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:120
#. translators: placeholders are opening and closing strong and link tags.
#. 1$-2$: strong tags, 3$-8$ link to docs on woocommerce
msgid ""
@@ -3067,14 +3231,14 @@ msgid ""
"%5$sCheck PayPal Account%6$s %3$sLearn more %7$s"
msgstr ""
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:132
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:136
#. translators: placeholders are opening and closing strong tags.
msgid ""
"%1$sPayPal Reference Transactions are enabled on your account%2$s. All "
"subscription management features are now enabled. Happy selling!"
msgstr ""
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:143
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:147
#. translators: placeholders are link opening and closing tags. 1$-2$: to
#. gateway settings, 3$-4$: support docs on woocommerce.com
msgid ""
@@ -3082,7 +3246,7 @@ msgid ""
"Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s."
msgstr ""
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:156
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:160
#. translators: placeholders are opening and closing link tags. 1$-2$: docs on
#. woocommerce, 3$-4$: dismiss link
msgid ""
@@ -3090,10 +3254,32 @@ msgid ""
"subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s."
msgstr ""
-#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:265
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:182
+msgid "Ignore this error (not recommended)"
+msgstr ""
+
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:187
+msgid "Open a ticket"
+msgstr ""
+
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:269
msgid "PayPal Subscription ID:"
msgstr ""
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:296
+msgid "Enable PayPal Standard for Subscriptions"
+msgstr ""
+
+#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:304
+#. translators: Placeholders are the opening and closing link tags.
+msgid ""
+"Before enabling PayPal Standard for Subscriptions, please note, when using "
+"PayPal Standard, customers are locked into using PayPal Standard for the "
+"life of their subscription, and PayPal Standard has a number of "
+"limitations. Please read the guide on %swhy we don't recommend PayPal "
+"Standard%s for Subscriptions before choosing to enable this option."
+msgstr ""
+
#: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:274
msgid "Total Discount"
msgstr ""
@@ -3116,32 +3302,32 @@ msgstr ""
msgid "Billing agreement cancelled at PayPal."
msgstr ""
-#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:278
+#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:279
msgid "IPN subscription sign up completed."
msgstr ""
#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:332
-#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:415
+#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:416
msgid "IPN subscription payment completed."
msgstr ""
-#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:377
+#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:378
msgid "IPN subscription failing payment method changed."
msgstr ""
-#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:467
+#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:468
msgid "IPN subscription suspended."
msgstr ""
-#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:490
+#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:491
msgid "IPN subscription cancelled."
msgstr ""
-#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:506
+#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:507
msgid "IPN subscription payment failure."
msgstr ""
-#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:644
+#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:645
msgid "Invalid PayPal IPN Payload: unable to find matching subscription."
msgstr ""
@@ -3435,13 +3621,13 @@ msgstr ""
msgid "Customers with a subscription are excluded from this setting."
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:330
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:332
#. translators: placeholder is a list of version numbers (e.g. "1.3 & 1.4 &
#. 1.5")
msgid "Database updated to version %s"
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:347
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:349
#. translators: 1$: number of action scheduler hooks upgraded, 2$:
#. "{execution_time}", will be replaced on front end with actual time
msgid ""
@@ -3449,28 +3635,29 @@ msgid ""
"seconds)."
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:359
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:361
#. translators: 1$: number of subscriptions upgraded, 2$: "{execution_time}",
#. will be replaced on front end with actual time it took
msgid "Migrated %1$s subscriptions to the new structure (in %2$s seconds)."
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:372
-#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:374
+#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag,
+#. 4$: break tag
msgid ""
-"Unable to upgrade subscriptions. Error: %1$s Please refresh the "
-"page and try again. If problem persists, %2$scontact support%3$s."
+"Unable to upgrade subscriptions.%4$sError: %1$s%4$sPlease refresh the page "
+"and try again. If problem persists, %2$scontact support%3$s."
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:623
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:625
msgid "Welcome to WooCommerce Subscriptions 2.1"
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:623
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:625
msgid "About WooCommerce Subscriptions"
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:804
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:806
msgid ""
"%1$sWarning!%2$s It appears that you have downgraded %1$sWooCommerce "
"Subscriptions%2$s from %3$s to %4$s. Downgrading the plugin in this way may "
@@ -3478,7 +3665,7 @@ msgid ""
"ticket%6$s for further assistance. %7$sLearn more »%8$s"
msgstr ""
-#: includes/upgrades/class-wc-subscriptions-upgrader.php:892
+#: includes/upgrades/class-wc-subscriptions-upgrader.php:875
msgid ""
"%1$sWarning!%2$s We discovered an issue in %1$sWooCommerce Subscriptions "
"2.3.0 - 2.3.2%2$s that may cause your subscription renewal order and "
@@ -3497,54 +3684,53 @@ msgid "Subscription end date in the past"
msgstr ""
#: includes/upgrades/class-wcs-upgrade-notice-manager.php:85
-msgid "New Subscription Coupon Features"
+msgid "New options to allow customers to sign up without a credit card"
msgstr ""
#: includes/upgrades/class-wcs-upgrade-notice-manager.php:86
msgid ""
-"Want to offer customers coupons which apply for 6 months? You can now "
-"define the number of cycles discounts would be applied."
+"Allow customers to access free trial and other $0 subscription products "
+"without needing to enter their credit card details on sign up."
msgstr ""
#: includes/upgrades/class-wcs-upgrade-notice-manager.php:89
-msgid "New Signup Pricing Options for Synchronized Subscriptions"
+msgid "Improved subscription payment method information"
msgstr ""
#: includes/upgrades/class-wcs-upgrade-notice-manager.php:90
msgid ""
-"Charge the full recurring price at the time of sign up for synchronized "
-"subscriptions. Your customers can now receive their products straight away."
+"Customers can now see more information about what payment method will be "
+"used for future payments."
msgstr ""
#: includes/upgrades/class-wcs-upgrade-notice-manager.php:93
-msgid "Link Parent Orders to Subscriptions"
+msgid "Auto-renewal toggle"
msgstr ""
-#: includes/upgrades/class-wcs-upgrade-notice-manager.php:95
-#. translators: placeholders are opening and closing tags linking to
-#. documentation.
+#: includes/upgrades/class-wcs-upgrade-notice-manager.php:94
msgid ""
-"For subscriptions with no parent order, shop managers can now choose a "
-"parent order via the Edit Subscription screen. This makes it possible to "
-"set a parent order on %smanually created subscriptions%s. The order can "
-"also be sent to customers to act as an invoice that needs to be paid to "
-"activate the subscription."
+"Enabled via a setting, this new feature will allow your customers to turn "
+"on and off automatic payments from the %sMy Account > View Subscription%s "
+"pages."
msgstr ""
-#: includes/upgrades/class-wcs-upgrade-notice-manager.php:99
+#: includes/upgrades/class-wcs-upgrade-notice-manager.php:97
+msgid "Update all subscription payment methods"
+msgstr ""
+
+#: includes/upgrades/class-wcs-upgrade-notice-manager.php:98
msgid ""
-"Customers can now renew their subscriptions before the scheduled next "
-"payment date. Why not use this to email your customers a coupon a month "
-"before their annual subscription renewals to get access to that revenue "
-"sooner?"
+"Customers will now have the option to update all their subscriptions when "
+"they are changing one of their subscription's payment methods - provided "
+"the payment gateway supports it."
msgstr ""
-#: includes/upgrades/class-wcs-upgrade-notice-manager.php:104
+#: includes/upgrades/class-wcs-upgrade-notice-manager.php:103
#. translators: placeholder is Subscription version string ('2.3')
msgid "Welcome to Subscriptions %s"
msgstr ""
-#: includes/upgrades/class-wcs-upgrade-notice-manager.php:111
+#: includes/upgrades/class-wcs-upgrade-notice-manager.php:110
msgid "Learn More"
msgstr ""
@@ -4137,7 +4323,7 @@ msgstr ""
#: includes/upgrades/templates/wcs-upgrade.php:43
msgid ""
"Customers and other non-administrative users can browse and purchase from "
-"your store without interuption while the update is in progress."
+"your store without interruption while the update is in progress."
msgstr ""
#: includes/upgrades/templates/wcs-upgrade.php:49
@@ -4160,7 +4346,7 @@ msgstr ""
msgid ""
"Remember, although the update process may take a while, customers and other "
"non-administrative users can browse and purchase from your store without "
-"interuption while the update is in progress."
+"interruption while the update is in progress."
msgstr ""
#: includes/upgrades/templates/wcs-upgrade.php:61
@@ -4271,7 +4457,7 @@ msgid "%1$s %2$s then %3$s on the %4$s day of every %5$s month"
msgstr ""
#: includes/wcs-formatting-functions.php:161
-#. translators: 1$: initial amount, 2$: intial description (e.g. "up front"),
+#. translators: 1$: initial amount, 2$: initial description (e.g. "up front"),
#. 3$: recurring amount, 4$: month of year (e.g. "March"), 5$: day of the month
#. (e.g. "23rd")
msgid "%1$s %2$s then %3$s on %4$s %5$s each year"
@@ -4304,6 +4490,11 @@ msgstr ""
msgid "%1$s free trial then %2$s"
msgstr ""
+#: includes/wcs-formatting-functions.php:228
+#. translators: placeholder is human time diff (e.g. "3 weeks")
+msgid "in %s"
+msgstr ""
+
#: includes/wcs-helper-functions.php:38
#. translators: date placeholder for input, javascript format
msgid "YYYY-MM-DD"
@@ -4421,38 +4612,30 @@ msgstr ""
msgid "Shipping Tax:"
msgstr ""
-#: templates/admin/html-failed-scheduled-action-notice.php:16
-#. translators: $1 and $2 are opening and closing link tags, respectively.
+#: templates/admin/html-failed-scheduled-action-notice.php:15
msgid ""
"An error has occurred while processing a recent subscription related event. "
-"Please %1$sopen a new ticket at WooCommerce Support%2$s immediately to get "
-"this resolved."
+"For steps on how to fix the affected subscription and to learn more about "
+"the possible causes of this error, please read our guide %1$shere%2$s."
msgid_plural ""
"An error has occurred while processing recent subscription related events. "
-"Please %1$sopen a new ticket at WooCommerce Support%2$s immediately to get "
-"this resolved."
+"For steps on how to fix the affected subscriptions and to learn more about "
+"the possible causes of this error, please read our guide %1$shere%2$s."
msgstr[0] ""
msgstr[1] ""
-#: templates/admin/html-failed-scheduled-action-notice.php:29
-#. translators: $1 and $2 are opening and closing link tags, respectively.
-msgid ""
-"To resolve this error as quickly as possible, please create a %1$stemporary "
-"administrator account%2$s with the user email support@prospress.com."
-msgstr ""
-
-#: templates/admin/html-failed-scheduled-action-notice.php:34
+#: templates/admin/html-failed-scheduled-action-notice.php:25
msgid "Affected event:"
msgid_plural "Affected events:"
msgstr[0] ""
msgstr[1] ""
-#: templates/admin/html-failed-scheduled-action-notice.php:41
+#: templates/admin/html-failed-scheduled-action-notice.php:32
#. translators: $1 the log file name $2 and $3 are opening and closing link
#. tags, respectively.
msgid ""
-"To see further details, view the %1$s log file from the %2$sWooCommerce "
-"logs screen.%2$s"
+"To see further details about these errors, view the %1$s log file from the "
+"%2$sWooCommerce logs screen.%2$s"
msgstr ""
#: templates/admin/html-variation-price.php:31
@@ -4481,13 +4664,18 @@ msgid ""
"or contact us if you need any help."
msgstr ""
-#: templates/checkout/form-change-payment-method.php:82
+#: templates/checkout/form-change-payment-method.php:92
msgid ""
"Sorry, it seems no payment gateways support changing the recurring payment "
"method. Please contact us if you require assistance or to make alternate "
"arrangements."
msgstr ""
+#: templates/checkout/form-change-payment-method.php:101
+#. translators: $1: opening tag, $2: closing tag
+msgid "Update the payment method used for %1$sall%2$s of my current subscriptions"
+msgstr ""
+
#: templates/checkout/recurring-totals.php:19
msgid "Recurring Totals"
msgstr ""
@@ -4627,19 +4815,19 @@ msgstr ""
msgid "ID"
msgstr ""
-#: templates/myaccount/my-subscriptions.php:70
+#: templates/myaccount/my-subscriptions.php:65
msgid "Previous"
msgstr ""
-#: templates/myaccount/my-subscriptions.php:74
+#: templates/myaccount/my-subscriptions.php:69
msgid "Next"
msgstr ""
-#: templates/myaccount/my-subscriptions.php:81
+#: templates/myaccount/my-subscriptions.php:76
msgid "You have reached the end of subscriptions. Go to the %sfirst page%s."
msgstr ""
-#: templates/myaccount/my-subscriptions.php:84
+#: templates/myaccount/my-subscriptions.php:79
#. translators: placeholders are opening and closing link tags to take to the
#. shop page
msgid ""
@@ -4651,7 +4839,7 @@ msgstr ""
msgid "Order"
msgstr ""
-#: templates/myaccount/related-orders.php:51
+#: templates/myaccount/related-orders.php:56
#. translators: $1: formatted order total for the order, $2: number of items
#. bought
msgid "%1$s for %2$d item"
@@ -4663,11 +4851,27 @@ msgstr[1] ""
msgid "Related Subscriptions"
msgstr ""
-#: templates/myaccount/subscription-details.php:43
+#: templates/myaccount/subscription-details.php:40
+msgid "Auto Renew"
+msgstr ""
+
+#: templates/myaccount/subscription-details.php:45
+msgid "Enable auto renew"
+msgstr ""
+
+#: templates/myaccount/subscription-details.php:50
+msgid "Disable auto renew"
+msgstr ""
+
+#: templates/myaccount/subscription-details.php:60
+msgid "Payment"
+msgstr ""
+
+#: templates/myaccount/subscription-details.php:70
msgid "Actions"
msgstr ""
-#: templates/myaccount/subscription-details.php:56
+#: templates/myaccount/subscription-details.php:83
msgid "Subscription Updates"
msgstr ""
@@ -4679,10 +4883,6 @@ msgstr ""
msgid "Are you sure you want remove this item from your subscription?"
msgstr ""
-#: templates/myaccount/view-subscription.php:20
-msgid "My Account"
-msgstr ""
-
#: templates/single-product/add-to-cart/subscription.php:45
#: templates/single-product/add-to-cart/variable-subscription.php:31
msgid "You have an active subscription to this product already."
@@ -4716,71 +4916,81 @@ msgstr ""
msgid "Date type can not be an empty string."
msgstr ""
-#: woocommerce-subscriptions.php:268
+#: woocommerce-subscriptions.php:269
msgid "This is where subscriptions are stored."
msgstr ""
-#: woocommerce-subscriptions.php:313
+#: woocommerce-subscriptions.php:314
msgid "No Subscriptions found"
msgstr ""
-#: woocommerce-subscriptions.php:315
+#: woocommerce-subscriptions.php:316
msgid ""
"Subscriptions will appear here for you to view and manage once purchased by "
"a customer."
msgstr ""
-#: woocommerce-subscriptions.php:317
+#: woocommerce-subscriptions.php:318
#. translators: placeholders are opening and closing link tags
msgid "%sLearn more about managing subscriptions »%s"
msgstr ""
-#: woocommerce-subscriptions.php:319
+#: woocommerce-subscriptions.php:320
#. translators: placeholders are opening and closing link tags
msgid "%sAdd a subscription product »%s"
msgstr ""
-#: woocommerce-subscriptions.php:475
+#: woocommerce-subscriptions.php:377
+msgid ""
+"To enable automatic renewals for this subscription, you will first need to "
+"add a payment method."
+msgstr ""
+
+#: woocommerce-subscriptions.php:377
+msgid "Would you like to add a payment method now?"
+msgstr ""
+
+#: woocommerce-subscriptions.php:493
msgid ""
"A subscription renewal has been removed from your cart. Multiple "
"subscriptions can not be purchased at the same time."
msgstr ""
-#: woocommerce-subscriptions.php:481
+#: woocommerce-subscriptions.php:499
msgid ""
"A subscription has been removed from your cart. Due to payment gateway "
"restrictions, different subscription products can not be purchased at the "
"same time."
msgstr ""
-#: woocommerce-subscriptions.php:487
+#: woocommerce-subscriptions.php:505
msgid ""
"A subscription has been removed from your cart. Products and subscriptions "
"can not be purchased at the same time."
msgstr ""
-#: woocommerce-subscriptions.php:629 woocommerce-subscriptions.php:646
+#: woocommerce-subscriptions.php:647 woocommerce-subscriptions.php:664
#. translators: placeholder is a number, this is for the teens
#. translators: placeholder is a number, numbers ending in 4-9, 0
msgid "%sth"
msgstr ""
-#: woocommerce-subscriptions.php:634
+#: woocommerce-subscriptions.php:652
#. translators: placeholder is a number, numbers ending in 1
msgid "%sst"
msgstr ""
-#: woocommerce-subscriptions.php:638
+#: woocommerce-subscriptions.php:656
#. translators: placeholder is a number, numbers ending in 2
msgid "%snd"
msgstr ""
-#: woocommerce-subscriptions.php:642
+#: woocommerce-subscriptions.php:660
#. translators: placeholder is a number, numbers ending in 3
msgid "%srd"
msgstr ""
-#: woocommerce-subscriptions.php:672
+#: woocommerce-subscriptions.php:690
#. translators: 1$-2$: opening and closing tags, 3$-4$: link tags,
#. takes to woocommerce plugin on wp.org, 5$-6$: opening and closing link tags,
#. leads to plugins.php in admin
@@ -4790,7 +5000,7 @@ msgid ""
"%5$sinstall & activate WooCommerce »%6$s"
msgstr ""
-#: woocommerce-subscriptions.php:675
+#: woocommerce-subscriptions.php:693
#. translators: 1$-2$: opening and closing tags, 3$: minimum supported
#. WooCommerce version, 4$-5$: opening and closing link tags, leads to plugin
#. admin
@@ -4800,11 +5010,11 @@ msgid ""
"WooCommerce to version %3$s or newer »%5$s"
msgstr ""
-#: woocommerce-subscriptions.php:706
+#: woocommerce-subscriptions.php:724
msgid "Variable Subscription"
msgstr ""
-#: woocommerce-subscriptions.php:801
+#: woocommerce-subscriptions.php:820
msgid ""
"%1$sWarning!%2$s We can see the %1$sWooCommerce Subscriptions Early "
"Renewal%2$s plugin is active. Version %3$s of %1$sWooCommerce "
@@ -4813,11 +5023,11 @@ msgid ""
"avoid any conflicts."
msgstr ""
-#: woocommerce-subscriptions.php:804
+#: woocommerce-subscriptions.php:823
msgid "Installed Plugins"
msgstr ""
-#: woocommerce-subscriptions.php:873
+#: woocommerce-subscriptions.php:892
#. translators: 1$-2$: opening and closing tags. 3$-4$: opening and
#. closing link tags for learn more. Leads to duplicate site article on docs.
#. 5$-6$: Opening and closing link to production URL. 7$: Production URL .
@@ -4829,19 +5039,19 @@ msgid ""
"the site's URL. %3$sLearn more »%4$s."
msgstr ""
-#: woocommerce-subscriptions.php:882
+#: woocommerce-subscriptions.php:901
msgid "Quit nagging me (but don't enable automatic payments)"
msgstr ""
-#: woocommerce-subscriptions.php:887
+#: woocommerce-subscriptions.php:906
msgid "Enable automatic payments"
msgstr ""
-#: woocommerce-subscriptions.php:1093
+#: woocommerce-subscriptions.php:1112
msgid "Support"
msgstr ""
-#: woocommerce-subscriptions.php:1176
+#: woocommerce-subscriptions.php:1195
#. translators: placeholders are opening and closing tags. Leads to docs on
#. version 2
msgid ""
@@ -4852,14 +5062,14 @@ msgid ""
"2.0 »%s"
msgstr ""
-#: woocommerce-subscriptions.php:1191
+#: woocommerce-subscriptions.php:1210
msgid ""
"Warning! You are running version %s of WooCommerce Subscriptions plugin "
"code but your database has been upgraded to Subscriptions version 2.0. This "
"will cause major problems on your store."
msgstr ""
-#: woocommerce-subscriptions.php:1192
+#: woocommerce-subscriptions.php:1211
msgid ""
"Please upgrade the WooCommerce Subscriptions plugin to version 2.0 or newer "
"immediately. If you need assistance, after upgrading to Subscriptions v2.0, "
@@ -4884,7 +5094,7 @@ msgstr ""
msgid "https://prospress.com/"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:206
+#: includes/admin/class-wc-subscriptions-admin.php:280
#. translators: placeholder is trial period validation message if passed an
#. invalid value (e.g. "Trial period can not exceed 4 weeks")
msgctxt "Trial period field tooltip on Edit Product administration screen"
@@ -4894,12 +5104,12 @@ msgid ""
"subscription. %s"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:219
+#: includes/admin/class-wc-subscriptions-admin.php:293
msgctxt "example price"
msgid "e.g. 5.90"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:253
+#: includes/admin/class-wc-subscriptions-admin.php:328
#: templates/admin/deprecated/html-variation-price.php:31
#: templates/admin/deprecated/html-variation-price.php:86
#: templates/admin/html-variation-price.php:21
@@ -4908,7 +5118,7 @@ msgctxt "example price"
msgid "e.g. 9.90"
msgstr ""
-#: includes/admin/class-wc-subscriptions-admin.php:790
+#: includes/admin/class-wc-subscriptions-admin.php:872
#. translators: placeholders are for HTML tags. They are 1$: "