PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.4.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.4.0
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
← All changes | includes/Frontend/Cart.php +299 -70 1.4.0 → 2.4.0 View file →
@@ -1,5 +1,10 @@
1 1 <?php
2 +/**
3 + * Cart handling for subscription products.
4 + *
5 + * @package SpringDevs\Subscription
6 + */
2 7
3 8 namespace SpringDevs\Subscription\Frontend;
4 9
5 10 use SpringDevs\Subscription\Illuminate\Helper;
@@ -6,8 +11,9 @@
6 11 use Automattic\WooCommerce\Blocks\Package;
7 12 use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
8 13 use Automattic\WooCommerce\StoreApi\Schemas\V1\CartItemSchema;
9 14 use Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema;
15 +use SpringDevs\Subscription\Illuminate\Subscription\Subscription;
10 16
11 17 /**
12 18 * Cart class
13 19 */
@@ -28,9 +34,11 @@
28 34 add_filter( 'woocommerce_get_item_data', array( $this, 'set_line_item_meta' ), 10, 2 );
29 35 add_action( 'woocommerce_before_calculate_totals', array( $this, 'add_calculation_price_filter' ) );
30 36 add_action( 'woocommerce_calculate_totals', array( $this, 'remove_calculation_price_filter' ) );
31 37 add_action( 'woocommerce_after_calculate_totals', array( $this, 'remove_calculation_price_filter' ) );
32 - add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'add_to_cart_validation' ), 10, 2 );
38 +
39 + add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'add_to_cart_validation' ), 10, 4 );
40 + add_action( 'woocommerce_store_api_validate_add_to_cart', array( $this, 'add_to_cart_validation_store_api' ), 10, 1 );
33 41 }
34 42
35 43 /**
36 44 * Add to cart validation.
@@ -36,38 +44,91 @@
36 44 * Add to cart validation.
37 45 *
38 46 * @param bool $passed Passed ?.
39 47 * @param int $product_id Product Id.
48 + * @param int $quantity Quantity.
49 + * @param int $variation_id Variation Id.
40 50 *
41 51 * @return bool
42 52 */
43 - public function add_to_cart_validation( bool $passed, int $product_id ): bool {
44 - $cart_items = WC()->cart->cart_contents;
53 + public function add_to_cart_validation( $passed, $product_id, $quantity, $variation_id = 0 ) {
54 + $product_id = (int) $variation_id > 0 ? (int) $variation_id : (int) $product_id;
55 + $validation = $this->validate_cart_items( $product_id );
56 +
57 + if ( $validation['failed'] ) {
58 + $error_notice = empty( $validation['error_notice'] ) ? __( 'This product cannot be added to the cart.', 'subscription' ) : $validation['error_notice'];
59 + wc_add_notice( $error_notice, 'error' );
60 + return false;
61 + }
62 +
63 + // Validation passed.
64 + return $passed;
65 + }
66 +
67 + /**
68 + * Add to cart validation.
69 + *
70 + * @param \WC_Product $product Product.
71 + * @throws \Exception If validation fails.
72 + */
73 + public function add_to_cart_validation_store_api( $product ) {
74 + $product_id = $product->get_id();
75 + $validation = $this->validate_cart_items( $product_id );
76 +
77 + if ( $validation['failed'] ) {
78 + $error_notice = empty( $validation['error_notice'] ) ? __( 'This product cannot be added to the cart.', 'subscription' ) : $validation['error_notice'];
79 + throw new \Exception( esc_html( $error_notice ) );
80 + }
81 + }
82 +
83 + /**
84 + * Validate cart items.
85 + *
86 + * @param int $product_id Product Id.
87 + * @return array
88 + */
89 + public function validate_cart_items( $product_id ) {
90 + /**
91 + * Filters whether the cart may hold more than one subscription, or a
92 + * subscription alongside a normal product, bypassing the restriction below.
93 + *
94 + * @param bool $allow Whether to allow it. Default false.
95 + * @param int $product_id Product being added to the cart.
96 + */
97 + if ( apply_filters( 'subscrpt_allow_multiple_subscriptions_in_cart', false, $product_id ) ) {
98 + return [
99 + 'failed' => false,
100 + 'error_notice' => null,
101 + ];
102 + }
103 +
104 + $cart_items = WC()->cart->cart_contents;
105 +
106 + $product = Subscription::get_subs_product( $product_id );
107 +
45 108 $error_notice = null;
46 109 $failed = false;
47 - $product = sdevs_get_subscription_product( $product_id );
48 - $enabled = $product->is_enabled();
110 + // A tied plan counts as a subscription even without classic `_subscrpt_enabled`.
111 + $enabled = $product->is_enabled() || subscrpt_plan_offered( $product_id );
112 +
49 113 foreach ( $cart_items as $key => $cart_item ) {
50 114 if ( isset( $cart_item['subscription'] ) ) {
51 115 if ( $enabled ) {
52 - $error_notice = __( 'Currently You have an another Subscriptional product on cart !!', 'sdevs_subscrpt' );
116 + $error_notice = __( 'You cannot purchase multiple subscriptions at the same time.', 'subscription' );
53 117 } else {
54 - $error_notice = __( 'Currently You have Subscriptional product in a cart !!', 'sdevs_subscrpt' );
118 + $error_notice = __( 'You cannot purchase a subscription and a non-subscription product at the same time.', 'subscription' );
55 119 }
56 120 $failed = true;
57 121 } elseif ( $enabled ) {
122 + $error_notice = __( 'You cannot purchase a subscription along with other products. Please remove other products from your cart first.', 'subscription' );
58 123 $failed = true;
59 - $error_notice = __( 'Your cart isn\'t empty !!', 'sdevs_subscrpt' );
60 124 }
61 125 }
62 126
63 - if ( $failed ) {
64 - wc_add_notice( $error_notice, 'error' );
65 -
66 - return false;
67 - }
68 -
69 - return $passed;
127 + return [
128 + 'failed' => (bool) $failed,
129 + 'error_notice' => $error_notice,
130 + ];
70 131 }
71 132
72 133 /**
73 134 * Add filter before cart calculation.
@@ -86,9 +147,9 @@
86 147 *
87 148 * @return float
88 149 */
89 150 public function set_prices_for_calculation( $price, $product ) {
90 - $product = sdevs_get_subscription_product( $product );
151 + $product = Subscription::get_subs_product( $product );
91 152 if ( $product->is_enabled() && $product->is_type( 'simple' ) ) {
92 153 $trial_time_per = $product->get_meta( '_subscrpt_trial_timing_per' );
93 154 if ( ! empty( $trial_time_per ) && $trial_time_per > 0 && Helper::check_trial( $product->get_id() ) ) {
94 155 return 0;
@@ -118,9 +179,9 @@
118 179 public function set_line_item_meta( $cart_item_data, $cart_item ) {
119 180 if ( isset( $cart_item['subscription'] ) ) {
120 181 if ( $cart_item['subscription']['trial'] ) {
121 182 $cart_item_data[] = array(
122 - 'key' => __( 'Free Trial', 'sdevs_subscrpt' ),
183 + 'key' => __( 'Free Trial', 'subscription' ),
123 184 'value' => $cart_item['subscription']['trial'],
124 185 'hidden' => true,
125 186 '__experimental_woocommerce_blocks_hidden' => false,
126 187 );
@@ -141,8 +202,16 @@
141 202 }
142 203 $cart_items = WC()->cart->cart_contents;
143 204 if ( is_array( $cart_items ) ) {
144 205 foreach ( $cart_items as $key => $value ) {
206 + // Plan items were validated against the plan by the resolver at
207 + // add-to-cart; their `subscription` snapshot intentionally differs
208 + // from the product's classic meta, so skip the classic re-check
209 + // (which would otherwise drop them from the cart).
210 + if ( ! empty( $value['subscrpt_plan_id'] ) ) {
211 + continue;
212 + }
213 +
145 214 /**
146 215 * Product Object.
147 216 *
148 217 * @var \WC_Product $product
@@ -147,24 +216,24 @@
147 216 *
148 217 * @var \WC_Product $product
149 218 */
150 219 $product = $value['data'];
151 - $product = sdevs_get_subscription_product( $product );
220 + $product = Subscription::get_subs_product( $product );
152 221 if ( isset( $value['subscription'] ) ) {
153 222 if ( $product->is_type( 'simple' ) ) {
154 223 if ( Helper::get_typos( 1, $product->get_meta( '_subscrpt_timing_option' ) ) !== $value['subscription']['type'] || $product->get_trial() !== $value['subscription']['trial'] ) {
155 224 // remove the item.
156 - wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'sdevs_subscrpt' ), 'error' );
225 + wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
157 226 WC()->cart->remove_cart_item( $key );
158 227 }
159 228 } else {
160 229 // remove the item.
161 - wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'sdevs_subscrpt' ), 'error' );
230 + wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
162 231 WC()->cart->remove_cart_item( $key );
163 232 }
164 - } elseif ( $product->get_meta( '_subscrpt_enabled' ) ) {
233 + } elseif ( $product->is_enabled() ) {
165 234 // remove the item.
166 - wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'sdevs_subscrpt' ), 'error' );
235 + wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
167 236 WC()->cart->remove_cart_item( $key );
168 237 }
169 238 }
170 239 }
@@ -203,37 +272,72 @@
203 272 */
204 273 public function extend_cart_schema() {
205 274 return array(
206 275 'recurring_totals' => array(
207 - 'description' => __( 'List of recurring totals in cart.', 'sdevs_subscrpt' ),
276 + 'description' => __( 'List of recurring totals in cart.', 'subscription' ),
208 277 'type' => 'array',
209 278 'readonly' => true,
210 279 'recurring_totals' => array(
211 - 'price' => array(
212 - 'description' => __( 'price of the subscription.', 'sdevs_subscrpt' ),
280 + 'price' => array(
281 + 'description' => __( 'price of the subscription, after any discount that applies to renewals.', 'subscription' ),
213 282 'type' => array( 'string' ),
214 283 'readonly' => true,
215 284 ),
216 - 'time' => array(
217 - 'description' => __( 'time of the subscription.', 'sdevs_subscrpt' ),
285 + 'full_price' => array(
286 + 'description' => __( 'price of the subscription before any discount.', 'subscription' ),
287 + 'type' => array( 'string' ),
288 + 'readonly' => true,
289 + ),
290 + 'first_price' => array(
291 + 'description' => __( 'amount charged today, after all discounts.', 'subscription' ),
292 + 'type' => array( 'string' ),
293 + 'readonly' => true,
294 + ),
295 + 'has_recurring_discount' => array(
296 + 'description' => __( 'Whether a discount applies to renewals.', 'subscription' ),
297 + 'type' => array( 'boolean' ),
298 + 'readonly' => true,
299 + ),
300 + 'has_one_time_discount' => array(
301 + 'description' => __( 'Whether a discount applies to the first payment only.', 'subscription' ),
302 + 'type' => array( 'boolean' ),
303 + 'readonly' => true,
304 + ),
305 + 'recurring_limit' => array(
306 + 'description' => __( 'Number of payments the recurring discount covers. 0 means unlimited.', 'subscription' ),
218 307 'type' => array( 'number' ),
219 308 'readonly' => true,
220 309 ),
221 - 'type' => array(
222 - 'description' => __( 'type of the subscription.', 'sdevs_subscrpt' ),
310 + 'time' => array(
311 + 'description' => __( 'time of the subscription.', 'subscription' ),
312 + 'type' => array( 'number' ),
313 + 'readonly' => true,
314 + ),
315 + 'type' => array(
316 + 'description' => __( 'type of the subscription.', 'subscription' ),
223 317 'type' => array( 'string' ),
224 318 'readonly' => true,
225 319 ),
226 - 'description' => array(
227 - 'description' => __( 'price of the subscription description.', 'sdevs_subscrpt' ),
320 + 'description' => array(
321 + 'description' => __( 'price of the subscription description.', 'subscription' ),
228 322 'type' => array( 'string' ),
229 323 'readonly' => true,
230 324 ),
231 - 'can_user_cancel' => array(
232 - 'description' => __( 'Can User Cancel?', 'sdevs_subscrpt' ),
325 + 'can_user_cancel' => array(
326 + 'description' => __( 'Allow User Cancellation?', 'subscription' ),
233 327 'type' => array( 'string' ),
234 328 'readonly' => true,
235 329 ),
330 + 'max_no_payment' => array(
331 + 'description' => __( 'Maximum Total Payments', 'subscription' ),
332 + 'type' => array( 'number' ),
333 + 'readonly' => true,
334 + ),
335 + 'split_total' => array(
336 + 'description' => __( 'Total price for a split-payment plan (entered plan price).', 'subscription' ),
337 + 'type' => array( 'number', 'null' ),
338 + 'readonly' => true,
339 + ),
236 340 ),
237 341 ),
238 342 );
239 343 }
@@ -246,9 +350,9 @@
246 350 public function extend_cart_data() {
247 351 $cart_items = WC()->cart->cart_contents;
248 352 $recurrings = array();
249 353 if ( $cart_items ) {
250 - foreach ( $cart_items as $cart_item ) {
354 + foreach ( $cart_items as $cart_item_key => $cart_item ) {
251 355 if ( isset( $cart_item['subscription'] ) && $cart_item['subscription']['type'] ) {
252 356 $cart_subscription = $cart_item['subscription'];
253 357 $start_date = Helper::start_date( $cart_subscription['trial'] );
254 358 $next_date = Helper::next_date(
@@ -255,16 +359,37 @@
255 359 ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'],
256 360 $cart_subscription['trial']
257 361 );
258 362
363 + // Subscription timing & type
364 + $time = $cart_subscription['time'];
365 + $type = Helper::get_typos( $time, $cart_subscription['type'], true );
366 +
367 + // Discount-aware totals, shared with the classic cart.
368 + $price_data = Helper::build_cart_recurring_price_data( $cart_item, $cart_item_key, $type );
369 +
370 + // Description
371 + $description = empty( $cart_subscription['trial'] )
372 + ? __( 'Next billing on', 'subscription' ) . ': ' . $next_date
373 + : __( 'First billing on', 'subscription' ) . ': ' . $start_date;
374 +
259 375 $recurrings[] = apply_filters(
260 376 'subscrpt_cart_recurring_data',
261 377 array(
262 - 'price' => ( $cart_item['subscription']['per_cost'] * $cart_item['quantity'] ) * 100,
263 - 'time' => $cart_subscription['time'],
264 - 'type' => $cart_subscription['type'],
265 - 'description' => empty( $cart_subscription['trial'] ) ? 'Next billing on: ' . $next_date : 'First billing on: ' . $start_date,
266 - 'can_user_cancel' => $cart_item['data']->get_meta( '_subscrpt_user_cancel' ),
378 + 'price' => $price_data['total'],
379 + 'full_price' => $price_data['full_total'],
380 + 'first_price' => $price_data['first_total'],
381 + 'has_recurring_discount' => $price_data['has_recurring_discount'],
382 + 'has_one_time_discount' => $price_data['has_one_time_discount'],
383 + 'recurring_limit' => $price_data['recurring_limit'],
384 + 'time' => $time,
385 + 'type' => $type,
386 + 'description' => $description,
387 + 'can_user_cancel' => Helper::can_user_cancel( $cart_item['data']->get_meta( '_subscrpt_user_cancel' ) ) ? 'yes' : 'no',
388 + 'max_no_payment' => ! empty( $cart_item['subscrpt_max_no_payment'] )
389 + ? (int) $cart_item['subscrpt_max_no_payment']
390 + : $cart_item['data']->get_meta( '_subscrpt_max_no_payment' ),
391 + 'split_total' => isset( $cart_item['subscrpt_split_total'] ) ? (float) $cart_item['subscrpt_split_total'] : null,
267 392 ),
268 393 $cart_item
269 394 );
270 395 }
@@ -280,33 +405,38 @@
280 405 * @return array Registered schema.
281 406 */
282 407 public function extend_cart_item_schema() {
283 408 return array(
284 - 'time' => array(
285 - 'description' => __( 'time of the subscription type.', 'sdevs_subscrpt' ),
409 + 'time' => array(
410 + 'description' => __( 'time of the subscription type.', 'subscription' ),
286 411 'type' => array( 'number', 'null' ),
287 412 'readonly' => true,
288 413 ),
289 - 'type' => array(
290 - 'description' => __( 'the subscription type.', 'sdevs_subscrpt' ),
414 + 'type' => array(
415 + 'description' => __( 'the subscription type.', 'subscription' ),
291 416 'type' => array( 'string', 'null' ),
292 417 'readonly' => true,
293 418 ),
294 - 'trial' => array(
295 - 'description' => __( 'the subscription trial.', 'sdevs_subscrpt' ),
419 + 'trial' => array(
420 + 'description' => __( 'the subscription trial.', 'subscription' ),
296 421 'type' => array( 'string', 'null' ),
297 422 'readonly' => true,
298 423 ),
299 - 'signup_fee' => array(
300 - 'description' => __( 'Signup Fee amount.', 'sdevs_subscrpt' ),
424 + 'signup_fee' => array(
425 + 'description' => __( 'Signup Fee amount.', 'subscription' ),
301 426 'type' => array( 'string', 'null' ),
302 427 'readonly' => true,
303 428 ),
304 - 'cost' => array(
305 - 'description' => __( 'Recurring amount.', 'sdevs_subscrpt' ),
429 + 'cost' => array(
430 + 'description' => __( 'Recurring amount.', 'subscription' ),
306 431 'type' => array( 'string', 'null' ),
307 432 'readonly' => true,
308 433 ),
434 + 'max_no_payment' => array(
435 + 'description' => __( 'Maximum Total Payments', 'subscription' ),
436 + 'type' => array( 'number' ),
437 + 'readonly' => true,
438 + ),
309 439 );
310 440 }
311 441
312 442 /**
@@ -317,18 +447,37 @@
317 447 * @return array $item_data Registered data or empty array if condition is not satisfied.
318 448 */
319 449 public function extend_cart_item_data( $cart_item ) {
320 450 $item_data = array(
321 - 'time' => null,
322 - 'type' => null,
323 - 'trial' => null,
324 - 'signup_fee' => null,
325 - 'cost' => null,
451 + 'time' => null,
452 + 'type' => null,
453 + 'trial' => null,
454 + 'signup_fee' => null,
455 + 'cost' => null,
456 + 'max_no_payment' => null,
326 457 );
458 +
327 459 if ( isset( $cart_item['subscription'] ) ) {
328 460 $item_data = $cart_item['subscription'];
329 461 unset( $item_data['per_cost'] );
330 - $item_data['cost'] = $cart_item['subscription']['per_cost'] * $cart_item['quantity'];
462 + $item_data['cost'] = (float) $cart_item['subscription']['per_cost'] * $cart_item['quantity'];
463 +
464 + // Plan items don't stamp the installment count into the subscription
465 + // array (it rides the cart item as subscrpt_max_no_payment); classic
466 + // products carry it on the product meta.
467 + if ( ! isset( $item_data['max_no_payment'] ) ) {
468 + $item_data['max_no_payment'] = ! empty( $cart_item['subscrpt_max_no_payment'] )
469 + ? (int) $cart_item['subscrpt_max_no_payment']
470 + : $cart_item['data']->get_meta( '_subscrpt_max_no_payment' );
471 + }
472 +
473 + // Normalise the cadence word to singular/plural by frequency for the
474 + // blocks (Store API) cart — plan items store the raw plural interval
475 + // (e.g. "months"), which the block would otherwise render as-is.
476 + if ( ! empty( $item_data['type'] ) ) {
477 + $sub_time = max( 1, (int) ( $item_data['time'] ?? 1 ) );
478 + $item_data['type'] = Helper::get_typos( $sub_time, $item_data['type'] );
479 + }
331 480 }
332 481 if ( ! subscrpt_pro_activated() ) {
333 482 $item_data['time'] = null;
334 483 $item_data['signup_fee'] = null;
@@ -345,12 +494,19 @@
345 494 *
346 495 * @return array
347 496 */
348 497 public function add_to_cart_item_data( array $cart_item_data, int $product_id ): array {
349 - $product = sdevs_get_subscription_product( $product_id );
498 + $product = Subscription::get_subs_product( $product_id );
350 499 if ( ! $product->is_type( 'simple' ) ) {
351 500 return $cart_item_data;
352 501 }
502 + // Plan products stamp their subscription snapshot in the plan checkout
503 + // (Frontend\PlanCheckout / Pro), gated on the chosen plan id — so a One-Time
504 + // purchase of a plan product is not wrongly tagged as a subscription here
505 + // (which would show a cadence + list it under "Recurring totals").
506 + if ( subscrpt_product_has_plan( $product_id ) ) {
507 + return $cart_item_data;
508 + }
353 509 if ( $product->is_enabled() ) :
354 510 $subscription_data = array();
355 511 $subscription_data['time'] = null;
356 512 $subscription_data['type'] = $product->get_timing_option();
@@ -357,11 +513,12 @@
357 513 $subscription_data['trial'] = null;
358 514 if ( $product->has_trial() ) {
359 515 $subscription_data['trial'] = $product->get_trial();
360 516 }
361 - $subscription_data['signup_fee'] = null;
362 - $subscription_data['per_cost'] = $product->get_price();
363 - $cart_item_data['subscription'] = apply_filters( 'subscrpt_block_simple_cart_item_data', $subscription_data, $product, $cart_item_data );
517 + $subscription_data['signup_fee'] = null;
518 + $subscription_data['per_cost'] = $product->get_price();
519 + $cart_item_data['subscription'] = apply_filters( 'subscrpt_block_simple_cart_item_data', $subscription_data, $product, $cart_item_data );
520 + $cart_item_data['subscription']['max_no_payment'] = $product->get_meta( '_subscrpt_max_no_payment' );
364 521 endif;
365 522
366 523 return $cart_item_data;
367 524 }
@@ -387,14 +544,17 @@
387 544 *
388 545 * @return string
389 546 */
390 547 public function change_price_cart_html( $price, $cart_item ) {
391 - $product = sdevs_get_subscription_product( $cart_item['product_id'] );
548 + $product = Subscription::get_subs_product( $cart_item['product_id'] );
392 549 if ( ! $product->is_type( 'simple' ) ) {
393 550 return $price;
394 551 }
395 552
396 - if ( $product->is_enabled() ) {
553 + // A tied plan makes it a subscription even without classic `_subscrpt_enabled`;
554 + // get_price_html() already resolves to the plan line (Frontend\Plans), so this
555 + // shows the plan cadence on the cart line without doubling.
556 + if ( $product->is_enabled() || subscrpt_plan_offered( $product->get_id() ) ) {
397 557 return $product->get_price_html();
398 558 }
399 559
400 560 return $price;
@@ -405,25 +565,88 @@
405 565 *
406 566 * @return void
407 567 */
408 568 public function add_rows_order_total() {
409 - $cart_items = WC()->cart->cart_contents;
410 - $recurrs = Helper::get_recurrs_for_cart( $cart_items );
569 + $cart_items = WC()->cart->get_cart_contents();
570 + $recurrs = Helper::get_recurrs_from_cart( $cart_items );
411 571 if ( 0 === count( $recurrs ) ) {
412 572 return;
413 573 }
414 574 ?>
415 575 <tr class="recurring-total">
416 - <th><?php esc_html_e( 'Recurring totals', 'sdevs_subscrpt' ); ?></th>
417 - <td data-title="<?php esc_attr_e( 'Recurring totals', 'sdevs_subscrpt' ); ?>">
576 + <th><?php esc_html_e( 'Recurring totals', 'subscription' ); ?></th>
577 + <td data-title="<?php esc_attr_e( 'Recurring totals', 'subscription' ); ?>">
418 578 <?php foreach ( $recurrs as $recurr ) : ?>
419 579 <p>
420 - <span><?php echo wp_kses_post( $recurr['price_html'] ); ?></span><br />
421 - <small><?php esc_html_e( ( $recurr['trial_status'] ? 'First billing on' : 'Next billing on' ), 'sdevs_subscrpt' ); ?>
422 - : <?php echo esc_html( $recurr['trial_status'] ? $recurr['start_date'] : $recurr['next_date'] ); ?></small>
423 - <?php if ( 'yes' === $recurr['can_user_cancel'] ) : ?>
580 + <span><?php echo wp_kses_post( $recurr['price_html'] ); ?></span>
581 + <?php if ( $recurr['max_no_payment'] > 0 ) : ?>
582 + <span>x <?php echo esc_html( $recurr['max_no_payment'] ); ?></span>
583 + <?php endif; ?>
584 + <br />
585 +
586 + <small>
587 + <?php
588 + $billing_text = $recurr['trial_status']
589 + ? __( 'First billing on', 'subscription' )
590 + : __( 'Next billing on', 'subscription' );
591 +
592 + echo esc_html( $billing_text . ': ' );
593 + echo esc_html( $recurr['trial_status'] ? $recurr['start_date'] : $recurr['next_date'] );
594 + ?>
595 + </small>
596 +
597 + <?php if ( ! empty( $recurr['has_one_time_discount'] ) ) : ?>
598 + <br />
599 + <small>
600 + <?php
601 + echo wp_kses_post(
602 + sprintf(
603 + // translators: 1: amount paid today, 2: amount charged on each renewal.
604 + __( 'You pay %1$s today. %2$s will be charged from the next renewal.', 'subscription' ),
605 + wc_price( $recurr['first_total'] ?? 0 ),
606 + wc_price( $recurr['total'] ?? 0 )
607 + )
608 + );
609 + ?>
610 + </small>
611 + <?php endif; ?>
612 +
613 + <?php if ( ! empty( $recurr['has_recurring_discount'] ) && (int) ( $recurr['recurring_limit'] ?? 0 ) > 1 ) : ?>
614 + <br />
615 + <small>
616 + <?php
617 + echo wp_kses_post(
618 + sprintf(
619 + // translators: 1: number of discounted payments, 2: full price charged afterwards.
620 + __( 'Discount applies to your first %1$s payments. After that, %2$s.', 'subscription' ),
621 + number_format_i18n( (int) $recurr['recurring_limit'] ),
622 + $recurr['full_price_html'] ?? ''
623 + )
624 + );
625 + ?>
626 + </small>
627 + <?php endif; ?>
628 +
629 + <?php if ( 'yes' === $recurr['can_user_cancel'] && 0 === (int) $recurr['max_no_payment'] ) : ?>
630 + <br />
631 + <small><?php esc_html_e( 'You can cancel subscription at any time!', 'subscription' ); ?></small>
632 + <?php endif; ?>
633 +
634 + <!-- add how many times will be build if _subscrpt_renewal_limit is not 0 -->
635 + <?php if ( (int) $recurr['max_no_payment'] > 0 ) : ?>
424 636 <br>
425 - <small><?php esc_html_e( 'You can cancel subscription at any time!', 'sdevs_subscrpt' ); ?></small>
637 + <small>
638 + <?php
639 + echo wp_kses_post(
640 + sprintf(
641 + // translators: 1: number of installments, 2: total amount.
642 + __( 'This subscription will be billed in %1$s installments, for a total of %2$s.', 'subscription' ),
643 + esc_html( $recurr['max_no_payment'] ),
644 + wc_price( isset( $recurr['split_total'] ) && null !== $recurr['split_total'] ? $recurr['split_total'] : $recurr['price'] * (int) $recurr['max_no_payment'] )
645 + )
646 + );
647 + ?>
648 + </small>
426 649 <?php endif; ?>
427 650 </p>
428 651 <?php endforeach; ?>
429 652 </td>
@@ -441,8 +664,14 @@
441 664 */
442 665 public function set_renew_status( $cart_item_data, $product_id ) {
443 666 $expired = Helper::subscription_exists( $product_id, 'expired' );
444 667 if ( $expired ) {
668 + // Check if maximum payment limit has been reached
669 + if ( subscrpt_is_max_payments_reached( $expired ) ) {
670 + wc_add_notice( __( 'This subscription has reached its maximum payment limit and cannot be renewed further.', 'subscription' ), 'error' );
671 + return $cart_item_data; // Don't add renew status
672 + }
673 +
445 674 $cart_item_data['renew_subscrpt'] = true;
446 675 }
447 676
448 677 return $cart_item_data;