Created
August 17, 2026 11:52
-
-
Save dwanjuki/e17e9bbab43f1f07e9782d052ec50161 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Deferred-payment promo: when a member of the eligible level checks out for | |
| * the level using the promo discount code, push their first recurring payment | |
| * out to 1 year after their existing subscription's next payment date. | |
| * They still pay the initial payment now. | |
| * | |
| * You can add this recipe to your site by creating a custom plugin | |
| * or using the Code Snippets plugin available for free in the WordPress repository. | |
| * Read this companion article for step-by-step directions on either method. | |
| * https://www.paidmembershipspro.com/create-a-plugin-for-pmpro-customizations/ | |
| */ | |
| /** | |
| * EDIT HERE: the discount code that triggers the promo. | |
| */ | |
| if ( ! defined( 'MY_PMPRO_PROMO_DISCOUNT_CODE' ) ) { | |
| define( 'MY_PMPRO_PROMO_DISCOUNT_CODE', 'ANNUAL99' ); | |
| } | |
| /** | |
| * EDIT HERE: the membership level ID the promo applies to. The member must | |
| * already hold this level, and be checking out for it again, to qualify. | |
| */ | |
| if ( ! defined( 'MY_PMPRO_PROMO_LEVEL_ID' ) ) { | |
| define( 'MY_PMPRO_PROMO_LEVEL_ID', 1 ); | |
| } | |
| /** | |
| * Resolve the discount code applied to a level object. | |
| * | |
| * @param object $level Level object. | |
| * @return string The applied discount code, or '' if none. | |
| */ | |
| function my_pmpro_get_level_discount_code( $level ) { | |
| if ( ! empty( $level->discount_code ) ) { | |
| return $level->discount_code; | |
| } | |
| if ( ! empty( $level->code_id ) ) { | |
| global $wpdb; | |
| return (string) $wpdb->get_var( | |
| $wpdb->prepare( "SELECT code FROM {$wpdb->pmpro_discount_codes} WHERE id = %d LIMIT 1", $level->code_id ) | |
| ); | |
| } | |
| return ''; | |
| } | |
| /** | |
| * Determine whether the promo applies to this level for the current user and, | |
| * if so, return the deferred first-payment date (UTC Y-m-d H:i:s). | |
| * | |
| * @param object $level The level being checked out / previewed. | |
| * @return string|false UTC datetime string, or false if the promo doesn't apply. | |
| */ | |
| function my_pmpro_promo_get_deferred_startdate( $level ) { | |
| // Bail if PMPro isn't loaded. | |
| if ( ! function_exists( 'pmpro_getMembershipLevelForUser' ) ) { | |
| return false; | |
| } | |
| // Only applies when checking out for the promo level (MY_PMPRO_PROMO_LEVEL_ID). | |
| if ( empty( $level ) || (int) $level->id !== (int) MY_PMPRO_PROMO_LEVEL_ID ) { | |
| return false; | |
| } | |
| // Only when the promo discount code is applied. Bail if the constant was | |
| // overridden with an empty value, otherwise every code-less checkout would match. | |
| $promo_code = trim( (string) MY_PMPRO_PROMO_DISCOUNT_CODE ); | |
| if ( '' === $promo_code ) { | |
| return false; | |
| } | |
| if ( strcasecmp( my_pmpro_get_level_discount_code( $level ), $promo_code ) !== 0 ) { | |
| return false; | |
| } | |
| // The level being purchased must be recurring for a deferral to mean anything. | |
| if ( ! pmpro_isLevelRecurring( $level ) ) { | |
| return false; | |
| } | |
| $user_id = get_current_user_id(); | |
| if ( ! $user_id ) { | |
| return false; | |
| } | |
| // Member must currently hold the promo level too (i.e. this is a renewal)... | |
| $current_level = pmpro_getMembershipLevelForUser( $user_id ); | |
| if ( empty( $current_level ) || (int) $current_level->id !== (int) MY_PMPRO_PROMO_LEVEL_ID ) { | |
| return false; | |
| } | |
| // ...and that current membership must itself be recurring. | |
| if ( ! pmpro_isLevelRecurring( $current_level ) ) { | |
| return false; | |
| } | |
| // Anchor the deferral to the member's existing subscription next payment date. | |
| $next_payment = pmpro_next_payment( $user_id, 'success', 'timestamp' ); | |
| if ( empty( $next_payment ) ) { | |
| // No scheduled next payment to anchor to (e.g. subscription already ended) — | |
| // fall back to 1 year from checkout so the promo still applies. | |
| $next_payment = time(); | |
| } | |
| // Push the first recurring payment to 1 year after the existing next payment date. | |
| $deferred = strtotime( '+1 year', (int) $next_payment ); | |
| // Gateways limit how far out the first payment can be scheduled: PayPal | |
| // clamps profile start dates to 1 year from now, and Stripe's maximum | |
| // trial period is 730 days. Clamp so checkout doesn't fail and the date | |
| // we display matches what the gateway will actually do. | |
| if ( in_array( pmpro_getGateway(), array( 'paypal', 'paypalexpress', 'paypalstandard' ), true ) ) { | |
| $max_deferred = strtotime( '+1 year' ); | |
| } else { | |
| $max_deferred = time() + 730 * DAY_IN_SECONDS; | |
| } | |
| return gmdate( 'Y-m-d H:i:s', min( $deferred, $max_deferred ) ); | |
| } | |
| /** | |
| * Defer the first recurring payment at checkout. | |
| */ | |
| function my_pmpro_promo_defer_checkout_startdate( $checkout_level ) { | |
| $startdate = my_pmpro_promo_get_deferred_startdate( $checkout_level ); | |
| if ( $startdate ) { | |
| $checkout_level->profile_start_date = $startdate; | |
| } | |
| return $checkout_level; | |
| } | |
| add_filter( 'pmpro_checkout_level', 'my_pmpro_promo_defer_checkout_startdate' ); | |
| /** | |
| * Show the expected next payment date in the level cost text so customers know. | |
| */ | |
| function my_pmpro_promo_show_startdate_in_cost_text( $cost, $level ) { | |
| // Prefer an already-set profile_start_date (full checkout); otherwise only | |
| // compute it during the discount-code preview request. | |
| $startdate = ! empty( $level->profile_start_date ) ? $level->profile_start_date : false; | |
| if ( empty( $startdate ) && ! empty( $_REQUEST['action'] ) && 'applydiscountcode' === $_REQUEST['action'] ) { | |
| $startdate = my_pmpro_promo_get_deferred_startdate( $level ); | |
| } | |
| if ( ! empty( $startdate ) ) { | |
| // profile_start_date is stored in UTC; convert to local time for display. | |
| $start_date = get_date_from_gmt( $startdate, get_option( 'date_format' ) ); | |
| $cost .= sprintf( ' <small>(%s %s)</small>', 'Next payment on', esc_html( $start_date ) ); | |
| } | |
| return $cost; | |
| } | |
| add_filter( 'pmpro_level_cost_text', 'my_pmpro_promo_show_startdate_in_cost_text', 10, 2 ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment