PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.11.2
Subscriptions for WooCommerce with Stripe Recurring Payments v1.11.2
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 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / includes / Illuminate / Cancellation.php

Cancellation.php in Subscriptions for WooCommerce with Stripe Recurring Payments 1.11.2, at includes/Illuminate/Cancellation.php

576 lines 20.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Subscription cancellation handler.
4 *
5 * Owns the conversion of a pending-cancellation (`pe_cancelled`) subscription
6 * into a fully `cancelled` one. This is intentionally kept separate from the
7 * hourly *expiry* check in {@see Cron} so cancellation-related behaviour can grow
8 * here independently.
9 *
10 * Default flow (free): when a subscription enters `pe_cancelled` it is scheduled
11 * to be cancelled 24 hours later. The exact moment is filterable via
12 * `subscrpt_cancellation_time`, which the Pro plugin uses to offer "immediately",
13 * "after 24 hours", or "at the end of the billing period".
14 *
15 * @package SpringDevs\Subscription\Illuminate
16 */
17
18 namespace SpringDevs\Subscription\Illuminate;
19
20 // Exit if accessed directly.
21 defined( 'ABSPATH' ) || exit;
22
23 /**
24 * Class Cancellation
25 *
26 * @package SpringDevs\Subscription\Illuminate
27 */
28 class Cancellation {
29
30 /**
31 * Post meta storing the timestamp at which a pending cancellation becomes final.
32 *
33 * @var string
34 */
35 const CANCEL_AT_META = '_subscrpt_cancel_at';
36
37 /**
38 * Initialize the class.
39 */
40 public function __construct() {
41 add_action( 'subscrpt_subscription_pending_cancellation', [ $this, 'schedule_cancellation' ] );
42 add_action( 'subscrpt_hourly_cron', [ $this, 'process_due_cancellations' ] );
43 add_action( 'subscrpt_subscription_resumed', [ $this, 'clear_scheduled_cancellation' ] );
44 add_action( 'before_single_subscrpt_content', [ $this, 'display_pending_cancellation_notice' ] );
45 add_action( 'before_single_subscrpt_content', [ $this, 'maybe_render_feedback_modal' ] );
46 add_action( 'wp_ajax_subscrpt_record_cancellation_feedback', [ $this, 'record_feedback' ] );
47 add_action( 'subscrpt_details_side_bottom', [ $this, 'render_admin_feedback_card' ] );
48 }
49
50 /**
51 * Fetch the most recent cancellation-feedback row for a subscription.
52 *
53 * @param int $subscription_id Subscription ID.
54 * @return array|null Associative row, or null when none exists.
55 */
56 public static function get_feedback( $subscription_id ) {
57 global $wpdb;
58
59 $row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
60 $wpdb->prepare(
61 "SELECT reason_label, comment, created_at FROM {$wpdb->prefix}subscrpt_cancellation_feedback WHERE subscription_id = %d ORDER BY id DESC LIMIT 1",
62 (int) $subscription_id
63 ),
64 ARRAY_A
65 );
66
67 return $row ? $row : null;
68 }
69
70 /**
71 * Show the customer's cancellation reason on the admin subscription details page
72 * (bottom of the right-hand column).
73 *
74 * Renders only for cancelled / pending-cancellation subscriptions that have a
75 * recorded feedback row.
76 *
77 * @param array $ctx Subscription details context.
78 * @return void
79 */
80 public function render_admin_feedback_card( $ctx ) {
81 $subscription_id = (int) ( $ctx['subscription_id'] ?? 0 );
82 $status = $ctx['status'] ?? '';
83
84 if ( ! in_array( $status, [ 'cancelled', 'pe_cancelled' ], true ) ) {
85 return;
86 }
87
88 $feedback = self::get_feedback( $subscription_id );
89 if ( ! $feedback ) {
90 return;
91 }
92
93 $reason = (string) ( $feedback['reason_label'] ?? '' );
94 $comment = (string) ( $feedback['comment'] ?? '' );
95 $when = '';
96 if ( ! empty( $feedback['created_at'] ) ) {
97 $when = date_i18n(
98 get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
99 strtotime( get_date_from_gmt( $feedback['created_at'] ) )
100 );
101 }
102 ?>
103 <div class="subscrpt-card">
104 <div class="subscrpt-card__head"><?php esc_html_e( 'Cancellation Reason', 'subscription' ); ?></div>
105 <div class="subscrpt-card__body">
106 <div style="display:flex;gap:10px;align-items:flex-start;">
107 <span style="flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;background:var(--wpsubs-brand-light);color:var(--wpsubs-brand);">
108 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
109 </span>
110 <div style="flex:1 1 auto;min-width:0;">
111 <p style="margin:0;font-weight:600;color:var(--wpsubs-text);word-break:break-word;">
112 <?php echo '' !== $reason ? esc_html( $reason ) : esc_html__( 'No reason selected', 'subscription' ); ?>
113 </p>
114 <?php if ( '' !== $when ) : ?>
115 <p style="margin:2px 0 0;font-size:12px;color:var(--wpsubs-text-subtle);">
116 <?php
117 printf(
118 /* translators: %s: date and time the feedback was submitted. */
119 esc_html__( 'Submitted %s', 'subscription' ),
120 esc_html( $when )
121 );
122 ?>
123 </p>
124 <?php endif; ?>
125 </div>
126 </div>
127 <?php if ( '' !== $comment ) : ?>
128 <blockquote style="margin:12px 0 0;padding:8px 12px;border-left:3px solid var(--wpsubs-border-strong);background:var(--wpsubs-surface-muted);border-radius:6px;color:var(--wpsubs-text-muted);font-size:13px;line-height:1.5;word-break:break-word;">
129 <?php echo wp_kses( nl2br( esc_html( $comment ) ), [ 'br' => [] ] ); ?>
130 </blockquote>
131 <?php endif; ?>
132 </div>
133 </div>
134 <?php
135 }
136
137 /**
138 * Render the cancellation-feedback modal on the single-subscription page.
139 *
140 * Only renders when the feature is enabled and the subscription is in a state
141 * that shows a Cancel button (pending/active/on_hold with user cancellation
142 * allowed). The markup is hidden by default; the frontend script intercepts the
143 * Cancel link click, opens it, and on confirm records feedback before following
144 * the original secure cancel URL.
145 *
146 * @param int $subscription_id Subscription ID.
147 * @return void
148 */
149 public function maybe_render_feedback_modal( $subscription_id ) {
150 if ( ! self::is_feedback_enabled() ) {
151 return;
152 }
153
154 $subscription_id = (int) $subscription_id;
155 $status = get_post_status( $subscription_id );
156
157 if ( ! in_array( $status, [ 'pending', 'active', 'on_hold' ], true ) ) {
158 return;
159 }
160
161 if ( 'no' === get_post_meta( $subscription_id, '_subscrpt_user_cancel', true ) ) {
162 return;
163 }
164
165 $reasons = self::get_reasons();
166 if ( empty( $reasons ) ) {
167 return;
168 }
169
170 wp_enqueue_style( 'subscrpt_cancellation_css', SUBSCRPT_ASSETS . '/css/cancellation.css', [], SUBSCRPT_VERSION );
171 wp_enqueue_script( 'subscrpt_cancellation_feedback', SUBSCRPT_ASSETS . '/js/frontend/cancellation-feedback.js', [], SUBSCRPT_VERSION, true );
172 wp_localize_script(
173 'subscrpt_cancellation_feedback',
174 'subscrptCancellationFeedback',
175 [
176 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
177 'nonce' => wp_create_nonce( 'subscrpt_cancellation_feedback' ),
178 ]
179 );
180 ?>
181 <div class="subscrpt-feedback-modal" id="subscrpt-feedback-modal" data-subscription="<?php echo esc_attr( $subscription_id ); ?>" hidden>
182 <div class="subscrpt-feedback-modal__overlay" data-subscrpt-feedback-dismiss></div>
183 <div class="subscrpt-feedback-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="subscrpt-feedback-title" aria-describedby="subscrpt-feedback-intro">
184 <div class="subscrpt-feedback-modal__header">
185 <h3 class="subscrpt-feedback-modal__title" id="subscrpt-feedback-title"><?php esc_html_e( 'Before you go', 'subscription' ); ?></h3>
186 <button type="button" class="subscrpt-feedback-modal__close" data-subscrpt-feedback-dismiss aria-label="<?php esc_attr_e( 'Close', 'subscription' ); ?>">
187 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>
188 </button>
189 </div>
190 <div class="subscrpt-feedback-modal__body">
191 <p class="subscrpt-feedback-modal__intro" id="subscrpt-feedback-intro"><?php esc_html_e( 'Please let us know why you are cancelling. Your feedback helps us improve.', 'subscription' ); ?></p>
192 <ul class="subscrpt-feedback-modal__reasons">
193 <?php foreach ( $reasons as $index => $reason ) : ?>
194 <?php
195 $reason_key = isset( $reason['key'] ) ? (string) $reason['key'] : '';
196 $reason_label = isset( $reason['label'] ) ? (string) $reason['label'] : '';
197 if ( '' === $reason_key || '' === $reason_label ) {
198 continue;
199 }
200 $input_id = 'subscrpt-feedback-reason-' . $index;
201 ?>
202 <li class="subscrpt-feedback-modal__reason">
203 <label class="subscrpt-feedback-modal__reason-label" for="<?php echo esc_attr( $input_id ); ?>">
204 <input type="radio" class="subscrpt-feedback-modal__radio" id="<?php echo esc_attr( $input_id ); ?>" name="subscrpt_feedback_reason" value="<?php echo esc_attr( $reason_key ); ?>" data-label="<?php echo esc_attr( $reason_label ); ?>" />
205 <span class="subscrpt-feedback-modal__reason-text"><?php echo esc_html( $reason_label ); ?></span>
206 </label>
207 </li>
208 <?php endforeach; ?>
209 </ul>
210 <?php if ( self::is_feedback_comment_enabled() ) : ?>
211 <textarea class="subscrpt-feedback-modal__comment" id="subscrpt-feedback-comment" rows="3" placeholder="<?php esc_attr_e( 'Additional comments (optional)', 'subscription' ); ?>"></textarea>
212 <?php endif; ?>
213 </div>
214 <div class="subscrpt-feedback-modal__footer">
215 <button type="button" class="subscrpt-feedback-modal__btn subscrpt-feedback-modal__keep" data-subscrpt-feedback-dismiss><?php esc_html_e( 'Keep subscription', 'subscription' ); ?></button>
216 <button type="button" class="subscrpt-feedback-modal__btn subscrpt-feedback-modal__confirm" id="subscrpt-feedback-confirm"><?php esc_html_e( 'Confirm cancellation', 'subscription' ); ?></button>
217 </div>
218 </div>
219 </div>
220 <?php
221 }
222
223 /**
224 * AJAX: record a cancellation-feedback submission.
225 *
226 * Best-effort — the frontend proceeds with cancellation regardless of the
227 * outcome. Verifies the nonce and that the current user owns the subscription
228 * (or is an admin), snapshots the reason label so historic rows survive later
229 * reason edits, stores the row, and fires `subscrpt_cancellation_feedback_recorded`.
230 *
231 * @return void
232 */
233 public function record_feedback() {
234 check_ajax_referer( 'subscrpt_cancellation_feedback', 'nonce' );
235
236 $subscription_id = isset( $_POST['subscription_id'] ) ? absint( wp_unslash( $_POST['subscription_id'] ) ) : 0;
237 if ( $subscription_id <= 0 ) {
238 wp_send_json_error( [ 'message' => 'invalid_subscription' ] );
239 }
240
241 $subs_post = get_post( $subscription_id );
242 if ( ! $subs_post || 'subscrpt_order' !== $subs_post->post_type ) {
243 wp_send_json_error( [ 'message' => 'invalid_subscription' ] );
244 }
245
246 $author_id = (int) $subs_post->post_author;
247 if ( ! current_user_can( 'manage_options' ) && $author_id !== get_current_user_id() ) {
248 wp_send_json_error( [ 'message' => 'forbidden' ] );
249 }
250
251 $reason_key = isset( $_POST['reason_key'] ) ? sanitize_key( wp_unslash( $_POST['reason_key'] ) ) : '';
252 $comment = isset( $_POST['comment'] ) ? sanitize_textarea_field( wp_unslash( $_POST['comment'] ) ) : '';
253
254 // Snapshot the label from the current reason set so it survives later edits.
255 $reason_label = '';
256 foreach ( self::get_reasons() as $reason ) {
257 if ( isset( $reason['key'] ) && (string) $reason['key'] === $reason_key ) {
258 $reason_label = isset( $reason['label'] ) ? (string) $reason['label'] : '';
259 break;
260 }
261 }
262
263 global $wpdb;
264 $table = $wpdb->prefix . 'subscrpt_cancellation_feedback';
265 $data = [
266 'subscription_id' => $subscription_id,
267 'customer_id' => $author_id,
268 'reason_key' => $reason_key,
269 'reason_label' => $reason_label,
270 'comment' => $comment,
271 'created_at' => current_time( 'mysql', true ),
272 ];
273
274 // One row per subscription: overwrite any previous feedback (e.g. after a
275 // reactivate → cancel-again cycle) rather than accumulating a log, so the
276 // row always reflects the latest cancellation reason.
277 $existing_id = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
278 $wpdb->prepare(
279 "SELECT id FROM {$wpdb->prefix}subscrpt_cancellation_feedback WHERE subscription_id = %d ORDER BY id DESC LIMIT 1",
280 $subscription_id
281 )
282 );
283
284 if ( $existing_id ) {
285 // Clear any stray duplicates from earlier writes, keep the one we update.
286 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
287 $wpdb->prepare(
288 "DELETE FROM {$wpdb->prefix}subscrpt_cancellation_feedback WHERE subscription_id = %d AND id <> %d",
289 $subscription_id,
290 $existing_id
291 )
292 );
293 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
294 $table,
295 $data,
296 [ 'id' => $existing_id ],
297 [ '%d', '%d', '%s', '%s', '%s', '%s' ],
298 [ '%d' ]
299 );
300 $data['id'] = $existing_id;
301 } else {
302 $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
303 $table,
304 $data,
305 [ '%d', '%d', '%s', '%s', '%s', '%s' ]
306 );
307 $data['id'] = (int) $wpdb->insert_id;
308 }
309
310 /**
311 * Fires after a cancellation-feedback row is stored.
312 *
313 * @param int $subscription_id Subscription ID.
314 * @param array $data Stored feedback row.
315 */
316 do_action( 'subscrpt_cancellation_feedback_recorded', $subscription_id, $data );
317
318 wp_send_json_success( [ 'id' => $data['id'] ] );
319 }
320
321 /**
322 * Get Settings
323 *
324 * @param string $id Setting ID.
325 */
326 public static function get_settings( $id = '' ) {
327 $settings = [
328 'subscrpt_cancellation_delay' => subscrpt_pro_activated() ? get_option( 'subscrpt_cancellation_delay', '24h' ) : '24h',
329 'subscrpt_cancellation_feedback_enabled' => get_option( 'subscrpt_cancellation_feedback_enabled', '1' ),
330 'subscrpt_cancellation_feedback_comment' => get_option( 'subscrpt_cancellation_feedback_comment', '1' ),
331 ];
332 return ! empty( $id ) ? $settings[ $id ] ?? false : $settings;
333 }
334
335 /**
336 * Whether the cancellation-feedback prompt is enabled.
337 *
338 * A free feature — works with or without Pro.
339 *
340 * @return bool
341 */
342 public static function is_feedback_enabled() {
343 return '1' === self::get_settings( 'subscrpt_cancellation_feedback_enabled' );
344 }
345
346 /**
347 * Whether the optional comment box is shown in the feedback form. Defaults to on.
348 *
349 * @return bool
350 */
351 public static function is_feedback_comment_enabled() {
352 return '1' === self::get_settings( 'subscrpt_cancellation_feedback_comment' );
353 }
354
355 /**
356 * Built-in default cancellation reasons.
357 *
358 * Used when no Pro-managed reason list is set. Filterable so Pro and
359 * integrations can adjust the defaults.
360 *
361 * @return array<int,array{key:string,label:string}>
362 */
363 public static function default_reasons() {
364 $reasons = [
365 [
366 'key' => 'too_expensive',
367 'label' => __( 'Too expensive', 'subscription' ),
368 ],
369 [
370 'key' => 'missing_features',
371 'label' => __( 'Missing features I need', 'subscription' ),
372 ],
373 [
374 'key' => 'found_alternative',
375 'label' => __( 'Found a better alternative', 'subscription' ),
376 ],
377 [
378 'key' => 'no_longer_needed',
379 'label' => __( 'No longer needed', 'subscription' ),
380 ],
381 [
382 'key' => 'technical_issues',
383 'label' => __( 'Technical issues', 'subscription' ),
384 ],
385 [
386 'key' => 'other',
387 'label' => __( 'Other', 'subscription' ),
388 ],
389 ];
390
391 /**
392 * Filter the built-in default cancellation reasons.
393 *
394 * @param array $reasons List of { key, label } reason entries.
395 */
396 return apply_filters( 'subscrpt_cancellation_default_reasons', $reasons );
397 }
398
399 /**
400 * Resolve the active cancellation reason list.
401 *
402 * Uses the Pro-managed `subscrpt_cancellation_reasons` option when Pro is
403 * active and the option is non-empty; otherwise falls back to the built-in
404 * defaults so the feature works without Pro.
405 *
406 * @return array<int,array{key:string,label:string}>
407 */
408 public static function get_reasons() {
409 if ( subscrpt_pro_activated() ) {
410 $reasons = get_option( 'subscrpt_cancellation_reasons', [] );
411 if ( ! empty( $reasons ) && is_array( $reasons ) ) {
412 return $reasons;
413 }
414 }
415
416 return self::default_reasons();
417 }
418
419 /**
420 * Record when a pending cancellation should become final.
421 *
422 * Runs whenever a subscription enters `pe_cancelled` (frontend, admin, or REST).
423 * If the resolved time is already due, the subscription is cancelled immediately.
424 *
425 * @param int $subscription_id Subscription ID.
426 * @return void
427 */
428 public function schedule_cancellation( $subscription_id ) {
429 $subscription_id = (int) $subscription_id;
430
431 /**
432 * Filter the timestamp at which a pending cancellation becomes a full cancellation.
433 *
434 * Return a Unix timestamp. A value at or before the current time cancels the
435 * subscription immediately. Defaults to 24 hours from now.
436 *
437 * @param int $cancel_at Unix timestamp for final cancellation.
438 * @param int $subscription_id Subscription ID.
439 */
440 $cancel_at = (int) apply_filters( 'subscrpt_cancellation_time', time() + DAY_IN_SECONDS, $subscription_id );
441
442 if ( $cancel_at <= time() ) {
443 $this->cancel( $subscription_id );
444 return;
445 }
446
447 update_post_meta( $subscription_id, self::CANCEL_AT_META, $cancel_at );
448 }
449
450 /**
451 * Hourly sweep: finalise any pending cancellations whose time has come.
452 *
453 * Picks up subscriptions whose `_subscrpt_cancel_at` is due, plus legacy
454 * `pe_cancelled` subscriptions (created before this meta existed) whose billing
455 * period has ended.
456 *
457 * @return void
458 */
459 public function process_due_cancellations() {
460 $subscriptions = get_posts(
461 [
462 'post_type' => 'subscrpt_order',
463 'post_status' => [ 'pe_cancelled' ],
464 'fields' => 'ids',
465 'numberposts' => -1,
466 'meta_query' => [
467 'relation' => 'OR',
468 [
469 'key' => self::CANCEL_AT_META,
470 'value' => time(),
471 'compare' => '<=',
472 'type' => 'NUMERIC',
473 ],
474 [
475 'relation' => 'AND',
476 [
477 'key' => self::CANCEL_AT_META,
478 'compare' => 'NOT EXISTS',
479 ],
480 [
481 'key' => '_subscrpt_next_date',
482 'value' => time(),
483 'compare' => '<=',
484 'type' => 'NUMERIC',
485 ],
486 ],
487 ],
488 ]
489 );
490
491 if ( empty( $subscriptions ) ) {
492 return;
493 }
494
495 // Ensure the mailer is ready so the cancellation email can be sent.
496 if ( function_exists( 'WC' ) && WC()->mailer() ) {
497 foreach ( $subscriptions as $subscription_id ) {
498 $this->cancel( (int) $subscription_id );
499 }
500 }
501 }
502
503 /**
504 * Finalise the cancellation of a single subscription.
505 *
506 * Guards against subscriptions that are no longer pending (e.g. reactivated).
507 *
508 * @param int $subscription_id Subscription ID.
509 * @return void
510 */
511 public function cancel( $subscription_id ) {
512 $subscription_id = (int) $subscription_id;
513
514 if ( 'pe_cancelled' === get_post_status( $subscription_id ) ) {
515 Action::status( 'cancelled', $subscription_id );
516 }
517
518 delete_post_meta( $subscription_id, self::CANCEL_AT_META );
519 }
520
521 /**
522 * Drop a scheduled cancellation when a subscription is reactivated.
523 *
524 * @param int $subscription_id Subscription ID.
525 * @return void
526 */
527 public function clear_scheduled_cancellation( $subscription_id ) {
528 delete_post_meta( (int) $subscription_id, self::CANCEL_AT_META );
529 }
530
531 /**
532 * Show a notice on the subscription details page when a cancellation is pending.
533 *
534 * Uses `_subscrpt_cancel_at` (the resolved final-cancellation time), falling back
535 * to the next renewal date for legacy subscriptions.
536 *
537 * @param int $subscription_id Subscription ID.
538 * @return void
539 */
540 public function display_pending_cancellation_notice( $subscription_id ) {
541 if ( 'pe_cancelled' !== get_post_status( $subscription_id ) ) {
542 return;
543 }
544
545 $cancel_at = (int) get_post_meta( $subscription_id, self::CANCEL_AT_META, true );
546 if ( ! $cancel_at ) {
547 $cancel_at = (int) get_post_meta( $subscription_id, '_subscrpt_next_date', true );
548 }
549
550 wp_enqueue_style( 'subscrpt_cancellation_css', SUBSCRPT_ASSETS . '/css/cancellation.css', [], SUBSCRPT_VERSION );
551 ?>
552 <div class="subscrpt-pending-cancel-notice" role="status">
553 <span class="subscrpt-pending-cancel-notice__icon" aria-hidden="true">
554 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
555 </span>
556 <div class="subscrpt-pending-cancel-notice__body">
557 <p class="subscrpt-pending-cancel-notice__text">
558 <?php
559 if ( $cancel_at ) {
560 $effective = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $cancel_at );
561 printf(
562 /* translators: %s: cancellation date and time. */
563 esc_html__( 'This subscription is scheduled to be cancelled on %s. You can continue accessing it until then.', 'subscription' ),
564 '<strong>' . esc_html( $effective ) . '</strong>'
565 );
566 } else {
567 esc_html_e( 'This subscription is scheduled to be cancelled at the end of the current billing period. You can continue accessing it until then.', 'subscription' );
568 }
569 ?>
570 </p>
571 </div>
572 </div>
573 <?php
574 }
575 }
576