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 +318 -101 1.3.1 → 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,37 +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( $passed, $product_id ) {
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 = wc_get_product( $product_id );
48 - $enabled = $product->get_meta( '_subscrpt_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 ) {
58 - $failed = true;
59 - $error_notice = __( 'Your cart isn\'t empty !!', 'sdevs_subscrpt' );
122 + $error_notice = __( 'You cannot purchase a subscription along with other products. Please remove other products from your cart first.', 'subscription' );
123 + $failed = true;
60 124 }
61 125 }
62 126
63 - if ( $failed ) {
64 - wc_add_notice( $error_notice, 'error' );
65 - return false;
66 - }
67 -
68 - return $passed;
127 + return [
128 + 'failed' => (bool) $failed,
129 + 'error_notice' => $error_notice,
130 + ];
69 131 }
70 132
71 133 /**
72 134 * Add filter before cart calculation.
@@ -85,14 +147,16 @@
85 147 *
86 148 * @return float
87 149 */
88 150 public function set_prices_for_calculation( $price, $product ) {
89 - if ( $product->get_meta( '_subscrpt_enabled' ) && $product->is_type( 'simple' ) ) {
151 + $product = Subscription::get_subs_product( $product );
152 + if ( $product->is_enabled() && $product->is_type( 'simple' ) ) {
90 153 $trial_time_per = $product->get_meta( '_subscrpt_trial_timing_per' );
91 154 if ( ! empty( $trial_time_per ) && $trial_time_per > 0 && Helper::check_trial( $product->get_id() ) ) {
92 155 return 0;
93 156 }
94 157 }
158 +
95 159 return $price;
96 160 }
97 161
98 162 /**
@@ -115,9 +179,9 @@
115 179 public function set_line_item_meta( $cart_item_data, $cart_item ) {
116 180 if ( isset( $cart_item['subscription'] ) ) {
117 181 if ( $cart_item['subscription']['trial'] ) {
118 182 $cart_item_data[] = array(
119 - 'key' => __( 'Free Trial', 'sdevs_subscrpt' ),
183 + 'key' => __( 'Free Trial', 'subscription' ),
120 184 'value' => $cart_item['subscription']['trial'],
121 185 'hidden' => true,
122 186 '__experimental_woocommerce_blocks_hidden' => false,
123 187 );
@@ -138,8 +202,16 @@
138 202 }
139 203 $cart_items = WC()->cart->cart_contents;
140 204 if ( is_array( $cart_items ) ) {
141 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 +
142 214 /**
143 215 * Product Object.
144 216 *
145 217 * @var \WC_Product $product
@@ -144,29 +216,24 @@
144 216 *
145 217 * @var \WC_Product $product
146 218 */
147 219 $product = $value['data'];
220 + $product = Subscription::get_subs_product( $product );
148 221 if ( isset( $value['subscription'] ) ) {
149 222 if ( $product->is_type( 'simple' ) ) {
150 - $product_trial_per = $product->get_meta( '_subscrpt_trial_timing_per' );
151 - $trial = null;
152 - if ( Helper::check_trial( $product->get_id() ) ) {
153 - $trial = ( $product_trial_per ? $product_trial_per . ' ' . Helper::get_typos( $product_trial_per ?? 1, $product->get_meta( '_subscrpt_trial_timing_option' ) )
154 - : null );
155 - }
156 - if ( Helper::get_typos( 1, $product->get_meta( '_subscrpt_timing_option' ) ) !== $value['subscription']['type'] || $trial !== $value['subscription']['trial'] ) {
223 + if ( Helper::get_typos( 1, $product->get_meta( '_subscrpt_timing_option' ) ) !== $value['subscription']['type'] || $product->get_trial() !== $value['subscription']['trial'] ) {
157 224 // remove the item.
158 - 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' );
159 226 WC()->cart->remove_cart_item( $key );
160 227 }
161 228 } else {
162 229 // remove the item.
163 - 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' );
164 231 WC()->cart->remove_cart_item( $key );
165 232 }
166 - } elseif ( $product->get_meta( '_subscrpt_enabled' ) ) {
233 + } elseif ( $product->is_enabled() ) {
167 234 // remove the item.
168 - 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' );
169 236 WC()->cart->remove_cart_item( $key );
170 237 }
171 238 }
172 239 }
@@ -205,37 +272,72 @@
205 272 */
206 273 public function extend_cart_schema() {
207 274 return array(
208 275 'recurring_totals' => array(
209 - 'description' => __( 'List of recurring totals in cart.', 'sdevs_subscrpt' ),
276 + 'description' => __( 'List of recurring totals in cart.', 'subscription' ),
210 277 'type' => 'array',
211 278 'readonly' => true,
212 279 'recurring_totals' => array(
213 - 'price' => array(
214 - 'description' => __( 'price of the subscription.', 'sdevs_subscrpt' ),
280 + 'price' => array(
281 + 'description' => __( 'price of the subscription, after any discount that applies to renewals.', 'subscription' ),
215 282 'type' => array( 'string' ),
216 283 'readonly' => true,
217 284 ),
218 - 'time' => array(
219 - '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' ),
220 307 'type' => array( 'number' ),
221 308 'readonly' => true,
222 309 ),
223 - 'type' => array(
224 - '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' ),
225 317 'type' => array( 'string' ),
226 318 'readonly' => true,
227 319 ),
228 - 'description' => array(
229 - 'description' => __( 'price of the subscription description.', 'sdevs_subscrpt' ),
320 + 'description' => array(
321 + 'description' => __( 'price of the subscription description.', 'subscription' ),
230 322 'type' => array( 'string' ),
231 323 'readonly' => true,
232 324 ),
233 - 'can_user_cancel' => array(
234 - 'description' => __( 'Can User Cancel?', 'sdevs_subscrpt' ),
325 + 'can_user_cancel' => array(
326 + 'description' => __( 'Allow User Cancellation?', 'subscription' ),
235 327 'type' => array( 'string' ),
236 328 'readonly' => true,
237 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 + ),
238 340 ),
239 341 ),
240 342 );
241 343 }
@@ -248,9 +350,9 @@
248 350 public function extend_cart_data() {
249 351 $cart_items = WC()->cart->cart_contents;
250 352 $recurrings = array();
251 353 if ( $cart_items ) {
252 - foreach ( $cart_items as $cart_item ) {
354 + foreach ( $cart_items as $cart_item_key => $cart_item ) {
253 355 if ( isset( $cart_item['subscription'] ) && $cart_item['subscription']['type'] ) {
254 356 $cart_subscription = $cart_item['subscription'];
255 357 $start_date = Helper::start_date( $cart_subscription['trial'] );
256 358 $next_date = Helper::next_date(
@@ -257,16 +359,37 @@
257 359 ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'],
258 360 $cart_subscription['trial']
259 361 );
260 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 +
261 375 $recurrings[] = apply_filters(
262 376 'subscrpt_cart_recurring_data',
263 377 array(
264 - 'price' => ( $cart_item['subscription']['per_cost'] * $cart_item['quantity'] ) * 100,
265 - 'time' => $cart_subscription['time'],
266 - 'type' => $cart_subscription['type'],
267 - 'description' => empty( $cart_subscription['trial'] ) ? 'Next billing on: ' . $next_date : 'First billing on: ' . $start_date,
268 - '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,
269 392 ),
270 393 $cart_item
271 394 );
272 395 }
@@ -271,8 +394,9 @@
271 394 );
272 395 }
273 396 }
274 397 }
398 +
275 399 return $recurrings;
276 400 }
277 401
278 402 /**
@@ -281,33 +405,38 @@
281 405 * @return array Registered schema.
282 406 */
283 407 public function extend_cart_item_schema() {
284 408 return array(
285 - 'time' => array(
286 - 'description' => __( 'time of the subscription type.', 'sdevs_subscrpt' ),
409 + 'time' => array(
410 + 'description' => __( 'time of the subscription type.', 'subscription' ),
287 411 'type' => array( 'number', 'null' ),
288 412 'readonly' => true,
289 413 ),
290 - 'type' => array(
291 - 'description' => __( 'the subscription type.', 'sdevs_subscrpt' ),
414 + 'type' => array(
415 + 'description' => __( 'the subscription type.', 'subscription' ),
292 416 'type' => array( 'string', 'null' ),
293 417 'readonly' => true,
294 418 ),
295 - 'trial' => array(
296 - 'description' => __( 'the subscription trial.', 'sdevs_subscrpt' ),
419 + 'trial' => array(
420 + 'description' => __( 'the subscription trial.', 'subscription' ),
297 421 'type' => array( 'string', 'null' ),
298 422 'readonly' => true,
299 423 ),
300 - 'signup_fee' => array(
301 - 'description' => __( 'Signup Fee amount.', 'sdevs_subscrpt' ),
424 + 'signup_fee' => array(
425 + 'description' => __( 'Signup Fee amount.', 'subscription' ),
302 426 'type' => array( 'string', 'null' ),
303 427 'readonly' => true,
304 428 ),
305 - 'cost' => array(
306 - 'description' => __( 'Recurring amount.', 'sdevs_subscrpt' ),
429 + 'cost' => array(
430 + 'description' => __( 'Recurring amount.', 'subscription' ),
307 431 'type' => array( 'string', 'null' ),
308 432 'readonly' => true,
309 433 ),
434 + 'max_no_payment' => array(
435 + 'description' => __( 'Maximum Total Payments', 'subscription' ),
436 + 'type' => array( 'number' ),
437 + 'readonly' => true,
438 + ),
310 439 );
311 440 }
312 441
313 442 /**
@@ -318,18 +447,37 @@
318 447 * @return array $item_data Registered data or empty array if condition is not satisfied.
319 448 */
320 449 public function extend_cart_item_data( $cart_item ) {
321 450 $item_data = array(
322 - 'time' => null,
323 - 'type' => null,
324 - 'trial' => null,
325 - 'signup_fee' => null,
326 - 'cost' => null,
451 + 'time' => null,
452 + 'type' => null,
453 + 'trial' => null,
454 + 'signup_fee' => null,
455 + 'cost' => null,
456 + 'max_no_payment' => null,
327 457 );
458 +
328 459 if ( isset( $cart_item['subscription'] ) ) {
329 460 $item_data = $cart_item['subscription'];
330 461 unset( $item_data['per_cost'] );
331 - $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 + }
332 480 }
333 481 if ( ! subscrpt_pro_activated() ) {
334 482 $item_data['time'] = null;
335 483 $item_data['signup_fee'] = null;
@@ -345,29 +493,34 @@
345 493 * @param int $product_id Product ID.
346 494 *
347 495 * @return array
348 496 */
349 - public function add_to_cart_item_data( $cart_item_data, $product_id ) {
350 - $product = wc_get_product( $product_id );
497 + public function add_to_cart_item_data( array $cart_item_data, int $product_id ): array {
498 + $product = Subscription::get_subs_product( $product_id );
351 499 if ( ! $product->is_type( 'simple' ) ) {
352 500 return $cart_item_data;
353 501 }
354 - $enabled = $product->get_meta( '_subscrpt_enabled' );
355 - if ( $enabled ) :
356 - $price_type = $product->get_meta( '_subscrpt_timing_option' );
357 - $type = Helper::get_typos( 1, $price_type );
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 + }
509 + if ( $product->is_enabled() ) :
358 510 $subscription_data = array();
359 511 $subscription_data['time'] = null;
360 - $subscription_data['type'] = $type;
512 + $subscription_data['type'] = $product->get_timing_option();
361 513 $subscription_data['trial'] = null;
362 - $trial_timing_per = $product->get_meta( '_subscrpt_trial_timing_per' );
363 - if ( $trial_timing_per && Helper::check_trial( $product->get_id() ) ) {
364 - $subscription_data['trial'] = $trial_timing_per . ' ' . Helper::get_typos( $trial_timing_per, $product->get_meta( '_subscrpt_trial_timing_option' ) );
514 + if ( $product->has_trial() ) {
515 + $subscription_data['trial'] = $product->get_trial();
365 516 }
366 - $subscription_data['signup_fee'] = null;
367 - $subscription_data['per_cost'] = $product->get_price();
368 - $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' );
369 521 endif;
522 +
370 523 return $cart_item_data;
371 524 }
372 525
373 526 /**
@@ -391,28 +544,21 @@
391 544 *
392 545 * @return string
393 546 */
394 547 public function change_price_cart_html( $price, $cart_item ) {
395 - $product = wc_get_product( $cart_item['product_id'] );
548 + $product = Subscription::get_subs_product( $cart_item['product_id'] );
396 549 if ( ! $product->is_type( 'simple' ) ) {
397 550 return $price;
398 551 }
399 552
400 - $enabled = $product->get_meta( '_subscrpt_enabled' );
401 - if ( $enabled ) :
402 - $has_trial = Helper::check_trial( $product->get_id() );
403 - $trial = null;
404 - $meta_trial_time = $product->get_meta( '_subscrpt_trial_timing_per' );
405 - if ( ! empty( $meta_trial_time ) && $meta_trial_time > 0 && $has_trial ) {
406 - $trial = '<br/><small> + Get ' . $meta_trial_time . ' ' . Helper::get_typos( $meta_trial_time, $product->get_meta( '_subscrpt_trial_timing_option' ) ) . ' free trial!</small>';
407 - }
408 - $timing_option = $product->get_meta( '_subscrpt_timing_option' );
409 - $type = Helper::get_typos( 1, $timing_option );
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() ) ) {
557 + return $product->get_price_html();
558 + }
410 559
411 - return apply_filters( 'subscrpt_simple_price_html', ( $price . '/ ' . $type . $trial ), $product, $price, $timing_option, $trial );
412 - else :
413 - return $price;
414 - endif;
560 + return $price;
415 561 }
416 562
417 563 /**
418 564 * Display "Recurring totals" on cart
@@ -419,26 +565,90 @@
419 565 *
420 566 * @return void
421 567 */
422 568 public function add_rows_order_total() {
423 - $cart_items = WC()->cart->cart_contents;
424 - $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 );
425 571 if ( 0 === count( $recurrs ) ) {
426 572 return;
427 573 }
428 574 ?>
429 575 <tr class="recurring-total">
430 - <th><?php esc_html_e( 'Recurring totals', 'sdevs_subscrpt' ); ?></th>
431 - <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' ); ?>">
432 578 <?php foreach ( $recurrs as $recurr ) : ?>
433 - <p>
434 - <span><?php echo wp_kses_post( $recurr['price_html'] ); ?></span><br />
435 - <small><?php echo esc_html_e( ( $recurr['trial_status'] ? 'First billing on' : 'Next billing on' ), 'sdevs_subscrpt' ); ?>: <?php echo esc_html( $recurr['trial_status'] ? $recurr['start_date'] : $recurr['next_date'] ); ?></small>
436 - <?php if ( 'yes' === $recurr['can_user_cancel'] ) : ?>
437 - <br>
438 - <small><?php echo esc_html_e( 'You can cancel subscription at any time!', 'sdevs_subscrpt' ); ?></small>
439 - <?php endif; ?>
440 - </p>
579 + <p>
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 ) : ?>
636 + <br>
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>
649 + <?php endif; ?>
650 + </p>
441 651 <?php endforeach; ?>
442 652 </td>
443 653 </tr>
444 654 <?php
@@ -454,9 +664,16 @@
454 664 */
455 665 public function set_renew_status( $cart_item_data, $product_id ) {
456 666 $expired = Helper::subscription_exists( $product_id, 'expired' );
457 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 +
458 674 $cart_item_data['renew_subscrpt'] = true;
459 675 }
676 +
460 677 return $cart_item_data;
461 678 }
462 679 }