is_type( 'simple' ) ) { return; } ?>
style="margin-bottom:16px;">
0, 'type' => 'recurring', ); require __DIR__ . '/../views/plans/modal-create.php'; require __DIR__ . '/../views/plans/modal-term.php'; } /** * Render the "Generate Checkout Link" modal for a plan-connected product. * * Builds a per-context (product / per-variation) list of the plans the product * offers — plus a "One-time purchase" option when enabled — and embeds it as a * JSON blob the modal JS uses to compose a direct add-to-cart or checkout link. * The link carries `subscrpt_plan_id` (the term id) so PlanCheckout resolves the * chosen plan; the empty id (one-time) omits it and relies on the bare-link * fallback. Renders nothing when the product has no plan connections. * * @param \WC_Product|null $product Product being edited. * * @return void */ public static function render_checkout_link_modal( $product ) { if ( ! $product || empty( PlanRepository::get_product_connections( $product->get_id() ) ) ) { return; } $is_variable = $product->is_type( 'variable' ); $product_id = $product->get_id(); // Plans a context offers, in the same order (and with the same ids) the // storefront resolver / generated link will use. One-time first when on. // One-time uses the sentinel value "onetime" (JS omits subscrpt_plan_id for // it) so the adv-select still resolves a non-empty default label. $build_plans = static function ( $vid, $one_time_enabled ) use ( $product_id ) { $plans = array(); if ( $one_time_enabled ) { $plans[] = array( 'value' => 'onetime', 'label' => __( 'One-time purchase', 'subscription' ), ); } foreach ( PlanRepository::resolve_for_product( $product_id, $vid ) as $subscrpt_row ) { $plans[] = array( 'value' => (string) (int) $subscrpt_row['plan_id'], 'label' => $subscrpt_row['group_title'] . ' · ' . $subscrpt_row['plan_title'], ); } return $plans; }; $contexts = array(); if ( $is_variable ) { foreach ( $product->get_children() as $subscrpt_child_id ) { $subscrpt_variation = wc_get_product( $subscrpt_child_id ); if ( ! $subscrpt_variation ) { continue; } $subscrpt_vid = (int) $subscrpt_child_id; $subscrpt_plans = $build_plans( $subscrpt_vid, 'yes' === $subscrpt_variation->get_meta( '_subscrpt_one_time_enabled' ) ); if ( empty( $subscrpt_plans ) ) { continue; } // An "Any …" attribute stores an empty value; such a variation can't // be resolved by id alone, so the checkout-link endpoint can't add it. $subscrpt_attrs = $subscrpt_variation->get_variation_attributes(); $subscrpt_has_any = in_array( '', array_map( 'strval', $subscrpt_attrs ), true ); $contexts[] = array( 'vid' => $subscrpt_vid, 'label' => self::variation_display_name( $subscrpt_variation, $product->get_name() ), 'attrs' => (object) $subscrpt_attrs, 'hasAny' => $subscrpt_has_any, 'plans' => $subscrpt_plans, ); } } else { $subscrpt_plans = $build_plans( 0, 'yes' === $product->get_meta( '_subscrpt_one_time_enabled' ) ); if ( ! empty( $subscrpt_plans ) ) { $contexts[] = array( 'vid' => 0, 'label' => '', 'attrs' => (object) array(), 'plans' => $subscrpt_plans, ); } } if ( empty( $contexts ) ) { return; } // JS only needs the ids/attributes to compose the link; plans are rendered // as adv-selects below, so keep the blob slim. $subscrpt_ctx_data = array(); foreach ( $contexts as $subscrpt_ctx ) { $subscrpt_ctx_data[] = array( 'vid' => $subscrpt_ctx['vid'], 'attrs' => $subscrpt_ctx['attrs'], 'hasAny' => ! empty( $subscrpt_ctx['hasAny'] ), ); } // Checkout link: WooCommerce Blocks' native checkout-link endpoint, which // empties the cart, adds `products=ID:QTY`, and redirects to checkout. It // resolves via the pretty path `/checkout-link/` when permalinks are on, // or the registered `?checkout-link=true` query var when they are plain. $subscrpt_checkout_base = get_option( 'permalink_structure' ) ? home_url( 'checkout-link/' ) : add_query_arg( 'checkout-link', 'true', home_url( '/' ) ); $subscrpt_data = array( 'productId' => $product_id, 'type' => $is_variable ? 'variable' : 'simple', // Add-to-cart link: WooCommerce's classic `?add-to-cart=` on the site root. 'cartBase' => home_url( '/' ), 'checkoutLinkBase' => $subscrpt_checkout_base, 'contexts' => $subscrpt_ctx_data, ); $subscrpt_field = 'display:flex;flex-direction:column;gap:6px;'; $subscrpt_field_l = 'font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em;color:var(--wpsubs-text-muted);'; $subscrpt_default_vid = (int) $contexts[0]['vid']; ?> is_type( 'variable' ) ) { $has_classic = false; foreach ( $product->get_children() as $variation_id ) { if ( function_exists( 'subscrpt_product_has_plan' ) && subscrpt_product_has_plan( $product->get_id(), $variation_id ) ) { return false; } // Ever plan-connected: stay in plan mode. if ( 'yes' === get_post_meta( $variation_id, '_subscrpt_plan_connected_before', true ) ) { return false; } if ( ! $has_classic ) { $variation = wc_get_product( $variation_id ); if ( $variation && (bool) $variation->get_meta( '_subscrpt_enabled' ) ) { $has_classic = true; } } } return $has_classic; } if ( function_exists( 'subscrpt_product_has_plan' ) && subscrpt_product_has_plan( $product->get_id() ) ) { return false; } // Ever plan-connected: stay in plan mode. if ( 'yes' === $product->get_meta( '_subscrpt_plan_connected_before' ) ) { return false; } return (bool) $product->get_meta( '_subscrpt_enabled' ); } /** * Render the view toggle bar (plan view ⇄ classic settings). * * @param \WC_Product|null $product Product being edited (null when no product context). * * @return void */ public static function render_toolbar( $product = null ) { // A connected plan enables the subscription by default (until saved off). $subscrpt_enabled = $product ? subscrpt_is_subscription_enabled( $product->get_id() ) : false; // Show the checkout-link generator only when the product is tied to a plan. $subscrpt_has_plans = $product && ! empty( PlanRepository::get_product_connections( $product->get_id() ) ); ?>
is_type( 'variable' ) ) ) : ?>
get_id() ); $connected = self::group_connections( $connections ); $available = self::available_groups( array_keys( $connected ) ); // Pro adds "create plan group / plan" shortcuts here; free is attach-only. $pro_active = function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated(); // One-time purchase (simple products): the product's native WooCommerce // price + an enabled flag, rendered as a row after the plan rows (matching // the variable-product layout), not a separate card. Null hides the row. $subscrpt_simple_ot = ( ! $product->is_type( 'variable' ) ) ? array( 'enabled' => 'yes' === $product->get_meta( '_subscrpt_one_time_enabled' ), 'regular' => \SpringDevs\Subscription\Admin\PlanPresenter::one_time_price( $product ), 'offer' => (string) $product->get_sale_price(), ) : null; ?>
$group ) : $all_terms = PlanRepository::get_plans( $subscrpt_gid ); ?>
is_type( 'variable' ) ) : ?>
is_type( 'variable' ) ) : ?>

(string) $group['id'], 'label' => $group['title'], ); } wpsubs_render_adv_select( array( 'name' => 'subscrpt_connect_group', 'placeholder' => __( 'Select a plan…', 'subscription' ), 'options' => $options, 'attrs' => array( 'data-subscrpt-connect-group' => '1' ), 'align' => 'right', ) ); ?>

is_type( 'variable' ) ) : ?>
relation values (empty for the connect view). * @param bool $connect Connect view: no relation ids, every term enabled by default. * @param int $vid Variation id these rows price (0 = the product itself). * @param array|null $one_time One-time purchase values for this variation * (enabled, regular, offer) — appended as a row; * null omits it (simple products use a separate card). * * @return void */ protected static function render_price_table( $all_terms, $term_map, $connect, $vid = 0, $one_time = null ) { ?> '', 'offer' => '', ); ?>
get_children() as $subscrpt_child_id ) { $subscrpt_vid = (int) $subscrpt_child_id; $subscrpt_variation = function_exists( 'wc_get_product' ) ? wc_get_product( $subscrpt_vid ) : null; if ( ! $subscrpt_variation ) { continue; } // Per-variation term map: the variation's own relations, falling back to // the product-level seed (with a blank relation id so Save creates this // variation's relation instead of editing the shared seed). $subscrpt_vmap = array(); if ( ! $connect ) { $subscrpt_own = isset( $map_by_vid[ $subscrpt_vid ] ) ? $map_by_vid[ $subscrpt_vid ] : array(); foreach ( $all_terms as $subscrpt_term ) { $subscrpt_pid = (int) $subscrpt_term['id']; if ( isset( $subscrpt_own[ $subscrpt_pid ] ) ) { $subscrpt_vmap[ $subscrpt_pid ] = $subscrpt_own[ $subscrpt_pid ]; } elseif ( isset( $seed[ $subscrpt_pid ] ) ) { $subscrpt_seed_row = $seed[ $subscrpt_pid ]; $subscrpt_seed_row['relation_id'] = ''; $subscrpt_vmap[ $subscrpt_pid ] = $subscrpt_seed_row; } } } // One-time purchase is the variation's own native WooCommerce price // (regular = one-time, sale = offer) + an enabled flag, saved with this // variation's plan prices on Save. $subscrpt_one_time = array( 'enabled' => 'yes' === $subscrpt_variation->get_meta( '_subscrpt_one_time_enabled' ), 'regular' => \SpringDevs\Subscription\Admin\PlanPresenter::one_time_price( $subscrpt_variation ), 'offer' => (string) $subscrpt_variation->get_sale_price(), ); // "Enable subscription" is per variation. Connecting a plan group turns // it on by default (until saved off); the connect toggle starts checked // and Save persists the variation's _subscrpt_enabled meta. $subscrpt_var_on = $connect ? true : subscrpt_is_subscription_enabled( $product->get_id(), $subscrpt_vid ); ?>
style="border:1px solid var(--wpsubs-border,#e5e7eb);border-radius:8px;background:var(--wpsubs-surface,#fff);">
get_name() ) ); ?>
relation values. * @param array|null $one_time One-time values (enabled, regular, offer). * @param int $vid Variation id (0 for simple), for the checkout link. * * @return void */ protected static function render_variation_summary( $all_terms, $vmap, $one_time, $vid = 0 ) { foreach ( $all_terms as $subscrpt_term ) : $subscrpt_tid = (int) $subscrpt_term['id']; $subscrpt_row = isset( $vmap[ $subscrpt_tid ] ) ? $vmap[ $subscrpt_tid ] : null; $subscrpt_off = ! $subscrpt_row || ! empty( $subscrpt_row['excluded'] ); $subscrpt_reg = $subscrpt_row ? (string) $subscrpt_row['regular'] : ''; $subscrpt_offer = $subscrpt_row ? (string) $subscrpt_row['offer'] : ''; ?> () get_variation_attributes() ) ); if ( $attributes ) { return implode( ', ', $attributes ); } $name = $variation->get_name(); return $name ? $name : $parent_name; } /** * Group flat relation rows by plan group. * * @param array $connections Rows from PlanRepository::get_product_connections(). * * @return array Keyed by group id: title, terms[], relation_ids[]. */ protected static function group_connections( $connections ) { $by_group = array(); foreach ( $connections as $row ) { $gid = (int) $row['plan_group_id']; if ( ! isset( $by_group[ $gid ] ) ) { $by_group[ $gid ] = array( 'title' => $row['group_title'], 'terms' => array(), 'relation_ids' => array(), 'term_map' => array(), 'term_map_by_vid' => array(), ); } $vid = (int) $row['vid']; $data = is_array( $row['relation_data'] ) ? $row['relation_data'] : array(); $price = isset( $data['regular_price'] ) ? (string) $data['regular_price'] : ''; $excluded = ! empty( $row['exclude'] ); $entry = array( 'relation_id' => (int) $row['relation_id'], 'excluded' => $excluded, 'regular' => $price, 'offer' => isset( $data['sale_price'] ) ? (string) $data['sale_price'] : '', ); $by_group[ $gid ]['relation_ids'][] = (int) $row['relation_id']; $by_group[ $gid ]['term_map_by_vid'][ $vid ][ (int) $row['plan_id'] ] = $entry; // Product-level (vid 0) rows drive the simple-product table + the read // summary chips; variation rows are consumed through term_map_by_vid. if ( 0 === $vid ) { $by_group[ $gid ]['terms'][] = array( 'title' => $row['plan_title'], 'price' => $price, 'relation_id' => (int) $row['relation_id'], 'excluded' => $excluded, ); $by_group[ $gid ]['term_map'][ (int) $row['plan_id'] ] = $entry; } } return $by_group; } /** * Plan groups not already connected to this product. * * Free is Recurring-only, so it offers only Recurring groups; Pro unlocks * Subscribe & Save and Installments, so all plan types are selectable. * * @param array $connected_ids Group ids already connected. * * @return array Each: id, title. */ protected static function available_groups( $connected_ids ) { $pro_active = function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated(); $recurring = PlanRepository::type_to_int( 'recurring' ); $available = array(); foreach ( PlanRepository::get_groups() as $group ) { if ( ! $pro_active && (int) $group['type'] !== $recurring ) { continue; } if ( 'trash' === ( $group['status'] ?? '' ) ) { continue; } if ( in_array( (int) $group['id'], array_map( 'intval', $connected_ids ), true ) ) { continue; } $available[] = array( 'id' => (int) $group['id'], 'title' => $group['title'], ); } return $available; } }