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 +149 -26 1.10.8 → 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;
@@ -44,10 +49,10 @@
44 49 * @param int $variation_id Variation Id.
45 50 *
46 51 * @return bool
47 52 */
48 - public function add_to_cart_validation( bool $passed, int $product_id, int $quantity, int $variation_id = 0 ): bool {
49 - $product_id = $variation_id > 0 ? $variation_id : $product_id;
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;
50 55 $validation = $this->validate_cart_items( $product_id );
51 56
52 57 if ( $validation['failed'] ) {
53 58 $error_notice = empty( $validation['error_notice'] ) ? __( 'This product cannot be added to the cart.', 'subscription' ) : $validation['error_notice'];
@@ -81,8 +86,22 @@
81 86 * @param int $product_id Product Id.
82 87 * @return array
83 88 */
84 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 +
85 104 $cart_items = WC()->cart->cart_contents;
86 105
87 106 $product = Subscription::get_subs_product( $product_id );
88 107
@@ -87,9 +106,10 @@
87 106 $product = Subscription::get_subs_product( $product_id );
88 107
89 108 $error_notice = null;
90 109 $failed = false;
91 - $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 );
92 112
93 113 foreach ( $cart_items as $key => $cart_item ) {
94 114 if ( isset( $cart_item['subscription'] ) ) {
95 115 if ( $enabled ) {
@@ -182,8 +202,16 @@
182 202 }
183 203 $cart_items = WC()->cart->cart_contents;
184 204 if ( is_array( $cart_items ) ) {
185 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 +
186 214 /**
187 215 * Product Object.
188 216 *
189 217 * @var \WC_Product $product
@@ -201,9 +229,9 @@
201 229 // remove the item.
202 230 wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
203 231 WC()->cart->remove_cart_item( $key );
204 232 }
205 - } elseif ( $product->get_meta( '_subscrpt_enabled' ) ) {
233 + } elseif ( $product->is_enabled() ) {
206 234 // remove the item.
207 235 wc_add_notice( __( 'An item which is no longer available was removed from your cart.', 'subscription' ), 'error' );
208 236 WC()->cart->remove_cart_item( $key );
209 237 }
@@ -248,38 +276,68 @@
248 276 'description' => __( 'List of recurring totals in cart.', 'subscription' ),
249 277 'type' => 'array',
250 278 'readonly' => true,
251 279 'recurring_totals' => array(
252 - 'price' => array(
253 - 'description' => __( 'price of the subscription.', 'subscription' ),
280 + 'price' => array(
281 + 'description' => __( 'price of the subscription, after any discount that applies to renewals.', 'subscription' ),
254 282 'type' => array( 'string' ),
255 283 'readonly' => true,
256 284 ),
257 - 'time' => array(
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' ),
307 + 'type' => array( 'number' ),
308 + 'readonly' => true,
309 + ),
310 + 'time' => array(
258 311 'description' => __( 'time of the subscription.', 'subscription' ),
259 312 'type' => array( 'number' ),
260 313 'readonly' => true,
261 314 ),
262 - 'type' => array(
315 + 'type' => array(
263 316 'description' => __( 'type of the subscription.', 'subscription' ),
264 317 'type' => array( 'string' ),
265 318 'readonly' => true,
266 319 ),
267 - 'description' => array(
320 + 'description' => array(
268 321 'description' => __( 'price of the subscription description.', 'subscription' ),
269 322 'type' => array( 'string' ),
270 323 'readonly' => true,
271 324 ),
272 - 'can_user_cancel' => array(
325 + 'can_user_cancel' => array(
273 326 'description' => __( 'Allow User Cancellation?', 'subscription' ),
274 327 'type' => array( 'string' ),
275 328 'readonly' => true,
276 329 ),
277 - 'max_no_payment' => array(
330 + 'max_no_payment' => array(
278 331 'description' => __( 'Maximum Total Payments', 'subscription' ),
279 332 'type' => array( 'number' ),
280 333 'readonly' => true,
281 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 + ),
282 340 ),
283 341 ),
284 342 );
285 343 }
@@ -292,9 +350,9 @@
292 350 public function extend_cart_data() {
293 351 $cart_items = WC()->cart->cart_contents;
294 352 $recurrings = array();
295 353 if ( $cart_items ) {
296 - foreach ( $cart_items as $cart_item ) {
354 + foreach ( $cart_items as $cart_item_key => $cart_item ) {
297 355 if ( isset( $cart_item['subscription'] ) && $cart_item['subscription']['type'] ) {
298 356 $cart_subscription = $cart_item['subscription'];
299 357 $start_date = Helper::start_date( $cart_subscription['trial'] );
300 358 $next_date = Helper::next_date(
@@ -301,17 +359,15 @@
301 359 ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'],
302 360 $cart_subscription['trial']
303 361 );
304 362
305 - // Total amount with tax
306 - $product = $cart_item['data'];
307 - $quantity = (int) $cart_item['quantity'];
308 - $total_amount = wc_get_price_including_tax( $product, [ 'qty' => $quantity ] );
309 -
310 363 // Subscription timing & type
311 364 $time = $cart_subscription['time'];
312 365 $type = Helper::get_typos( $time, $cart_subscription['type'], true );
313 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 +
314 370 // Description
315 371 $description = empty( $cart_subscription['trial'] )
316 372 ? __( 'Next billing on', 'subscription' ) . ': ' . $next_date
317 373 : __( 'First billing on', 'subscription' ) . ': ' . $start_date;
@@ -318,14 +374,22 @@
318 374
319 375 $recurrings[] = apply_filters(
320 376 'subscrpt_cart_recurring_data',
321 377 array(
322 - 'price' => $total_amount,
323 - 'time' => $time,
324 - 'type' => $type,
325 - 'description' => $description,
326 - 'can_user_cancel' => $cart_item['data']->get_meta( '_subscrpt_user_cancel' ),
327 - 'max_no_payment' => $cart_item['data']->get_meta( '_subscrpt_max_no_payment' ),
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,
328 392 ),
329 393 $cart_item
330 394 );
331 395 }
@@ -395,8 +459,25 @@
395 459 if ( isset( $cart_item['subscription'] ) ) {
396 460 $item_data = $cart_item['subscription'];
397 461 unset( $item_data['per_cost'] );
398 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 + }
399 480 }
400 481 if ( ! subscrpt_pro_activated() ) {
401 482 $item_data['time'] = null;
402 483 $item_data['signup_fee'] = null;
@@ -417,8 +498,15 @@
417 498 $product = Subscription::get_subs_product( $product_id );
418 499 if ( ! $product->is_type( 'simple' ) ) {
419 500 return $cart_item_data;
420 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 + }
421 509 if ( $product->is_enabled() ) :
422 510 $subscription_data = array();
423 511 $subscription_data['time'] = null;
424 512 $subscription_data['type'] = $product->get_timing_option();
@@ -461,9 +549,12 @@
461 549 if ( ! $product->is_type( 'simple' ) ) {
462 550 return $price;
463 551 }
464 552
465 - 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() ) ) {
466 557 return $product->get_price_html();
467 558 }
468 559
469 560 return $price;
@@ -501,9 +592,41 @@
501 592 echo esc_html( $billing_text . ': ' );
502 593 echo esc_html( $recurr['trial_status'] ? $recurr['start_date'] : $recurr['next_date'] );
503 594 ?>
504 595 </small>
505 -
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 +
506 629 <?php if ( 'yes' === $recurr['can_user_cancel'] && 0 === (int) $recurr['max_no_payment'] ) : ?>
507 630 <br />
508 631 <small><?php esc_html_e( 'You can cancel subscription at any time!', 'subscription' ); ?></small>
509 632 <?php endif; ?>
@@ -517,9 +640,9 @@
517 640 sprintf(
518 641 // translators: 1: number of installments, 2: total amount.
519 642 __( 'This subscription will be billed in %1$s installments, for a total of %2$s.', 'subscription' ),
520 643 esc_html( $recurr['max_no_payment'] ),
521 - wc_price( $recurr['price'] * (int) $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'] )
522 645 )
523 646 );
524 647 ?>
525 648 </small>