- Use on up to — live sites
$currency, 'type' => 'visible', 'is_enriched' => 'true', ], $endpoint ); $cache_key = 'bld_go_pt_fs_' . md5( $url ); if ( $cache_ttl > 0 ) { $cached = get_transient( $cache_key ); if ( $cached ) { return $cached; } } $args = [ 'timeout' => 20, 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $bearer, ], ]; $resp = wp_remote_get( $url, $args ); if ( is_wp_error( $resp ) ) { return $resp; } $code = (int) wp_remote_retrieve_response_code( $resp ); if ( $code < 200 || $code >= 300 ) { return new WP_Error( 'go_pt_fs_http_' . $code, 'Freemius API error: HTTP ' . $code ); } $body = wp_remote_retrieve_body( $resp ); $data = json_decode( $body, true ); if ( ! is_array( $data ) ) { return new WP_Error( 'go_pt_fs_bad_json', 'Freemius API returned invalid JSON.' ); } if ( $cache_ttl > 0 ) { set_transient( $cache_key, $data, $cache_ttl ); } return $data; } /** * Fetch a single Freemius coupon (enriched) for the given product. * * @param string $product_id The Freemius product ID. * @param string $coupon_id The Freemius coupon ID. * @param string $currency The currency code (default is 'USD'). Supported values are 'USD', 'EUR', and 'GBP'. * @param int $cache_ttl The time-to-live for the cached response in seconds (default is 21600). * @param string $bearer The Freemius API token for authentication. * * @return array|WP_Error The coupon details as an associative array on success, or a WP_Error object on failure. */ public function fetch_freemius_coupon( $product_id, $coupon_id, $currency = 'USD', $cache_ttl = 21600, $bearer = '' ) { $product_id = trim( $product_id ); $coupon_id = trim( $coupon_id ); if ( '' === $product_id || '' === $coupon_id ) { return new WP_Error( 'go_pt_fs_coupon_missing_params', 'Missing Freemius product_id or coupon_id.' ); } if ( '' === trim( $bearer ) ) { return new WP_Error( 'go_pt_fs_coupon_missing_token', 'Missing Freemius API token.' ); } $endpoint = sprintf( 'https://api.freemius.com/v1/products/%s/coupons/%s.json', rawurlencode( $product_id ), rawurlencode( $coupon_id ) ); $currency = strtoupper( trim( $currency ) ); if ( ! in_array( $currency, [ 'USD', 'EUR', 'GBP' ], true ) ) { $currency = 'USD'; } $url = add_query_arg( [ 'is_enriched' => 'true', 'currency' => $currency, ], $endpoint ); $cache_key = 'bld_go_pt_fs_coupon_' . md5( $url ); if ( $cache_ttl > 0 ) { $cached = get_transient( $cache_key ); if ( $cached ) { return $cached; } } $args = [ 'timeout' => 20, 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $bearer, ], ]; $resp = wp_remote_get( $url, $args ); if ( is_wp_error( $resp ) ) { return $resp; } $code = (int) wp_remote_retrieve_response_code( $resp ); if ( $code < 200 || $code >= 300 ) { return new WP_Error( 'go_pt_fs_coupon_http_' . $code, 'Freemius API error: HTTP ' . $code ); } $body = wp_remote_retrieve_body( $resp ); $data = json_decode( $body, true ); if ( ! is_array( $data ) ) { return new WP_Error( 'go_pt_fs_coupon_bad_json', 'Freemius API returned invalid JSON for coupon.' ); } if ( $cache_ttl > 0 ) { set_transient( $cache_key, $data, $cache_ttl ); } return $data; } /** * Parse a comma-separated list of coupon IDs. * * @param string $raw Raw string. * @return array Sanitized list of coupon IDs (strings). */ public function parse_coupon_ids( $raw ) { $out = []; foreach ( explode( ',', (string) $raw ) as $id ) { $id = trim( $id ); if ( '' === $id ) { continue; } $out[] = $id; } return array_values( array_unique( $out ) ); } /** * Parse a discount string like "$10" or "15%". * * @param string $raw Raw discount string. * @return array|null Array with keys type ('dollar'|'percentage') and amount (float) or null if invalid. */ public function parse_discount_string( $raw ) { $raw = trim( (string) $raw ); if ( '' === $raw ) { return null; } if ( preg_match( '/^\\$\\s*([0-9]+(?:\\.[0-9]+)?)$/', $raw, $m ) ) { return [ 'type' => 'dollar', 'amount' => (float) $m[1], ]; } if ( preg_match( '/^([0-9]+(?:\\.[0-9]+)?)\\s*%$/', $raw, $m ) ) { return [ 'type' => 'percentage', 'amount' => (float) $m[1], ]; } return null; } /** * Normalize a coupon response into a structured array for evaluation. * * @param array $coupon Raw coupon array. * * @return array Normalized coupon details. */ public function normalize_coupon_record( $coupon ) { if ( ! is_array( $coupon ) ) { return [ 'is_valid' => false ]; } $discount_type = strtolower( (string) ( $coupon['discount_type'] ?? '' ) ); if ( ! in_array( $discount_type, [ 'percentage', 'dollar' ], true ) ) { $discount_type = ''; } $discount_val = $this->money_to_float( $coupon['discount'] ?? 0 ); $discount_map = []; if ( isset( $coupon['discounts'] ) && is_array( $coupon['discounts'] ) ) { foreach ( [ 'usd', 'eur', 'gbp' ] as $cur ) { if ( isset( $coupon['discounts'][ $cur ] ) ) { $discount_map[ $cur ] = $this->money_to_float( $coupon['discounts'][ $cur ] ); } } } $plans_raw = isset( $coupon['plans'] ) ? explode( ',', (string) $coupon['plans'] ) : []; $plans_list = []; foreach ( $plans_raw as $pid ) { $pid = trim( $pid ); if ( '' !== $pid ) { $plans_list[] = $pid; } } $licenses_raw = isset( $coupon['licenses'] ) ? explode( ',', (string) $coupon['licenses'] ) : []; $licenses_list = []; foreach ( $licenses_raw as $lic ) { $lic = trim( $lic ); if ( '' !== $lic ) { $licenses_list[] = (int) $lic; } } $cycles_raw = isset( $coupon['billing_cycles'] ) ? explode( ',', (string) $coupon['billing_cycles'] ) : []; $cycles = []; foreach ( $cycles_raw as $c ) { $c = trim( $c ); if ( '' === $c ) { continue; } $label = $this->coupon_cycle_label_from_value( $c ); if ( $label ) { $cycles[] = $label; } } $start_ts = null; if ( ! empty( $coupon['start_date'] ) ) { $tmp = strtotime( (string) $coupon['start_date'] ); if ( false !== $tmp ) { $start_ts = $tmp; } } $end_ts = null; if ( ! empty( $coupon['end_date'] ) ) { $tmp = strtotime( (string) $coupon['end_date'] ); if ( false !== $tmp ) { $end_ts = $tmp; } } $redemptions = (int) ( $coupon['redemptions'] ?? 0 ); $redemptions_limit = isset( $coupon['redemptions_limit'] ) ? (int) $coupon['redemptions_limit'] : null; $is_active = ! empty( $coupon['is_active'] ); $now = time(); if ( $start_ts && $now < $start_ts ) { $is_active = false; } if ( $end_ts && $now > $end_ts ) { $is_active = false; } if ( null !== $redemptions_limit && $redemptions >= $redemptions_limit ) { $is_active = false; } $code = (string) ( $coupon['code'] ?? '' ); return [ 'is_valid' => $is_active && '' !== $code && '' !== $discount_type && ( $discount_val > 0 || ! empty( $discount_map ) ), 'id' => (string) ( $coupon['id'] ?? '' ), 'code' => $code, 'discount_type' => $discount_type, 'discount' => $discount_val, 'discounts' => $discount_map, 'plans' => $plans_list, 'licenses' => $licenses_list, 'cycles' => $cycles, 'redemptions_limit' => $redemptions_limit, 'redemptions' => $redemptions, 'start_ts' => $start_ts, 'end_ts' => $end_ts, ]; } /** * Map Freemius billing cycle values to internal labels. * * @param string|int $val Billing cycle value. * @return string|null */ private function coupon_cycle_label_from_value( $val ) { $val = (string) $val; if ( '1' === $val || 'monthly' === strtolower( $val ) ) { return 'Monthly'; } if ( '12' === $val || 'annual' === strtolower( $val ) || 'yearly' === strtolower( $val ) ) { return 'Annual'; } return null; } /** * Determine if a coupon applies to a plan. * * @param array $coupon Normalized coupon data. * @param string $plan_id Current plan ID. * @return bool */ private function coupon_supports_plan( $coupon, $plan_id ) { if ( empty( $coupon['plans'] ) ) { return true; } return in_array( (string) $plan_id, $coupon['plans'], true ); } /** * Determine if a coupon applies to the requested licenses count. * * @param array $coupon Normalized coupon data. * @param int $licenses License count. * @return bool */ private function coupon_supports_license( $coupon, $licenses ) { if ( empty( $coupon['licenses'] ) ) { return true; } if ( in_array( 0, $coupon['licenses'], true ) ) { return true; } return in_array( (int) $licenses, $coupon['licenses'], true ); } /** * Determine if a coupon applies to the billing cycle. * * @param array $coupon Normalized coupon data. * @param string $cycle Cycle label ('Monthly'|'Annual'). * @return bool */ private function coupon_supports_cycle( $coupon, $cycle ) { if ( empty( $coupon['cycles'] ) ) { return true; } return in_array( $cycle, $coupon['cycles'], true ); } /** * Get the fixed discount value for a currency, if present. * * @param array $coupon Normalized coupon data. * @param string $currency Currency code (lowercase). * @return float */ private function coupon_fixed_amount( $coupon, $currency ) { if ( isset( $coupon['discounts'][ $currency ] ) ) { return (float) $coupon['discounts'][ $currency ]; } return (float) ( $coupon['discount'] ?? 0 ); } /** * Apply stacked coupons to plan pricing. * * @param array $plans Plans array. * @param array $coupons Normalized coupon array. * @param string $currency Currency code (lowercase). * @return array Adjusted pricing map. */ public function apply_coupons_to_pricing( $plans, $coupons, $currency = 'usd' ) { $currency = strtolower( $currency ); $out = []; foreach ( $plans as $pl ) { $plan_id = (string) ( $pl['plan_id'] ?? '' ); if ( '' === $plan_id ) { continue; } $out[ $plan_id ] = []; foreach ( $pl['terms'] as $licenses => $term_row ) { $current_monthly = isset( $term_row['Monthly'] ) ? $this->money_to_float( $term_row['Monthly'] ) : null; $current_annual = isset( $term_row['Annual'] ) ? $this->money_to_float( $term_row['Annual'] ) : null; $licenses_int = (int) $licenses; foreach ( [ 'Monthly', 'Annual' ] as $cycle ) { if ( 'Monthly' === $cycle && null === $current_monthly ) { continue; } if ( 'Annual' === $cycle && null === $current_annual ) { continue; } $base_price = ( 'Monthly' === $cycle ) ? $current_monthly : $current_annual; $final = $base_price; $applied = []; foreach ( $coupons as $coupon ) { if ( empty( $coupon['is_valid'] ) ) { continue; } if ( ! $this->coupon_supports_plan( $coupon, $plan_id ) ) { continue; } if ( ! $this->coupon_supports_license( $coupon, $licenses_int ) ) { continue; } if ( ! $this->coupon_supports_cycle( $coupon, $cycle ) ) { continue; } $amount_off = 0.0; if ( 'percentage' === $coupon['discount_type'] ) { $amount_off = $final * ( (float) ( $coupon['discount'] ?? 0 ) / 100 ); } elseif ( 'dollar' === $coupon['discount_type'] ) { $amount_off = $this->coupon_fixed_amount( $coupon, $currency ); } $amount_off = max( 0.0, $amount_off ); $amount_off = min( $final, $amount_off ); $final -= $amount_off; if ( $amount_off > 0 ) { $applied[] = [ 'id' => $coupon['id'], 'code' => $coupon['code'], 'type' => $coupon['discount_type'], 'scope' => empty( $coupon['plans'] ) ? 'global' : 'plan', 'amount_off' => $amount_off, 'cycle' => $cycle, 'license' => $licenses_int, 'plan_id' => $plan_id, 'base_price' => $base_price, 'final_price' => $final, ]; } } $out[ $plan_id ][ $licenses ][ $cycle ] = [ 'base' => $base_price, 'final' => $final, 'applied' => $applied, ]; } } } return $out; } /** * Transforms Freemius pricing data into a structured format for product plans. * * @param array $fs_data The Freemius data containing product and pricing information. * @param array $labels_override Optional. An associative array of custom label overrides for site tiers. * * @return array An associative array containing 'product' details and 'plans' information. */ public function transform_fs_pricing_to_plans( $fs_data, $labels_override = [] ) { $product = [ 'product_name' => '', 'public_key' => '', ]; if ( isset( $fs_data['plugin'] ) && is_array( $fs_data['plugin'] ) ) { $product['product_name'] = (string) ( $fs_data['plugin']['title'] ?? '' ); $product['public_key'] = (string) ( $fs_data['plugin']['public_key'] ?? '' ); } $plans_out = []; $best_plan_idx = -1; $best_plan_pct = -1; $plans = $fs_data['plans'] ?? []; foreach ( $plans as $idx => $pl ) { if ( ! is_array( $pl ) ) { continue; } $plan_id = isset( $pl['id'] ) ? (string) $pl['id'] : ''; $plan_title = (string) ( $pl['title'] ?? ( $pl['name'] ?? '' ) ); if ( '' === $plan_id || '' === $plan_title ) { continue; } $pricing = is_array( $pl['pricing'] ?? null ) ? $pl['pricing'] : []; $site_tiers = []; $terms = []; foreach ( $pricing as $p_idx => $p ) { if ( ! is_array( $p ) ) { continue; } $licenses = (int) ( $p['licenses'] ?? 0 ); if ( $licenses <= 0 ) { continue; } if ( isset( $labels_override[ $p_idx ] ) ) { $label = (string) $labels_override[ $p_idx ]; } else { $label = ( 1 === $licenses ) ? 'Single Site' : ( 'Up to ' . $licenses . ' Sites' ); } $site_tiers[ $label ] = (string) $licenses; $row = []; if ( isset( $p['monthly_price'] ) && '' !== $p['monthly_price'] ) { $row['Monthly'] = (float) $p['monthly_price']; } if ( isset( $p['annual_price'] ) && '' !== $p['annual_price'] ) { $row['Annual'] = (float) $p['annual_price']; } if ( ! empty( $row ) ) { $terms[ (string) $licenses ] = $row; } } if ( empty( $terms ) ) { continue; } $has_trial = false; if ( isset( $pl['trial_period'] ) ) { $has_trial = ( (int) $pl['trial_period'] ) > 0; } $plan_pct = 0; foreach ( $terms as $row ) { if ( isset( $row['Monthly'], $row['Annual'] ) && $row['Monthly'] > 0 && $row['Annual'] > 0 ) { $pct = round( max( 0, ( $row['Monthly'] * 12 ) - $row['Annual'] ) / ( $row['Monthly'] * 12 ) * 100 ); if ( $pct > $plan_pct ) { $plan_pct = $pct; } } } $desc = (string) ( $pl['description'] ?? '' ); $features = []; if ( isset( $pl['features'] ) && is_array( $pl['features'] ) ) { foreach ( $pl['features'] as $f ) { if ( is_array( $f ) ) { $title = (string) ( $f['title'] ?? ( $f['name'] ?? '' ) ); if ( '' !== $title ) { $features[] = $title; } } } } $is_featured = ! empty( $pl['is_featured'] ); if ( ! $is_featured && $plan_pct > $best_plan_pct ) { $best_plan_pct = $plan_pct; $best_plan_idx = $idx; } $plans_out[] = [ 'plan_name' => $plan_title, 'plan_id' => $plan_id, 'best_value' => $is_featured, 'site_tiers' => $site_tiers, 'terms' => $terms, 'desc' => $desc, 'features' => $features, 'has_trial' => $has_trial, ]; } $any_featured = false; foreach ( $plans_out as $p ) { if ( ! empty( $p['best_value'] ) ) { $any_featured = true; break; } } if ( ! $any_featured && $best_plan_idx >= 0 && isset( $plans_out[ $best_plan_idx ] ) ) { $plans_out[ $best_plan_idx ]['best_value'] = true; } return [ 'product' => $product, 'plans' => $plans_out, ]; } /** * Parses the provided content to extract and process child shortcodes, generating structured data for plans. * * @param string $content Content string to parse for child shortcodes. * @return array An array of parsed plan details, including metadata, features, site tiers, terms, and other attributes. */ protected function parse_child_shortcodes( $content ) { if ( '' === trim( $content ) ) { return []; } $plans = []; $pattern = get_shortcode_regex( [ 'go_plan_column' ] ); if ( preg_match_all( '/' . $pattern . '/s', $content, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $m ) { // $m[3] is the attributes string, $m[5] is the inner content (unused here) $atts_raw = $m[3] ?? ''; $atts = shortcode_parse_atts( $atts_raw ); if ( ! is_array( $atts ) ) { $atts = []; } // Normalize keys to lowercase to make attributes case-insensitive. $norm = []; foreach ( $atts as $k => $v ) { $norm[ strtolower( (string) $k ) ] = is_scalar( $v ) ? (string) $v : ''; } $plan_name = $norm['plan_name'] ?? ''; $plan_id = $norm['plan_id'] ?? ''; $site_tiers_label = $norm['site_tiers_label'] ?? ( $norm['site_tierslabel'] ?? '' ); $site_tiers_vals = $norm['site_tiers'] ?? ( $norm['site_tiersvalue'] ?? '' ); $monthly = $norm['monthly'] ?? ''; $annual = $norm['annual'] ?? ''; $plan_desc = $norm['plan_desc'] ?? ''; $plan_features = $norm['plan_features'] ?? ''; $best_value_raw = $norm['best_value'] ?? ''; $has_trial_raw = $norm['has_trial'] ?? 'true'; $plan_coupon = $norm['coupon'] ?? ''; $plan_discount = $norm['discount'] ?? ''; $labels = array_map( 'trim', $this->parse_pipe_list( $site_tiers_label ) ); $tiers = array_map( 'trim', $this->parse_pipe_list( $site_tiers_vals ) ); $monthly_a = array_map( 'trim', $this->parse_pipe_list( $monthly ) ); $annual_a = array_map( 'trim', $this->parse_pipe_list( $annual ) ); $features = $this->parse_pipe_list( $plan_features ); $site_tiers_map = []; $count = max( count( $labels ), count( $tiers ) ); for ( $i = 0; $i < $count; $i++ ) { $label = $labels[ $i ] ?? ( $tiers[ $i ] ?? (string) ( $i + 1 ) ); $val = $tiers[ $i ] ?? (string) ( $i + 1 ); $site_tiers_map[ $label ] = $val; } $terms = []; $max_terms = max( count( $tiers ), count( $monthly_a ), count( $annual_a ) ); for ( $i = 0; $i < $max_terms; $i++ ) { $site = isset( $tiers[ $i ] ) ? (string) $tiers[ $i ] : (string) ( $i + 1 ); $row = []; if ( isset( $monthly_a[ $i ] ) && '' !== $monthly_a[ $i ] ) { $row['Monthly'] = $monthly_a[ $i ]; } if ( isset( $annual_a[ $i ] ) && '' !== $annual_a[ $i ] ) { $row['Annual'] = $annual_a[ $i ]; } if ( ! empty( $row ) ) { $terms[ $site ] = $row; } } $best_value = false; if ( '' !== $best_value_raw ) { $bv = strtolower( trim( $best_value_raw ) ); $best_value = in_array( $bv, [ '1', 'true', 'yes', 'on' ], true ); } $has_trial = true; if ( '' !== $has_trial_raw ) { $ht = strtolower( trim( $has_trial_raw ) ); $has_trial = ! in_array( $ht, [ '0', 'false', 'no', 'off' ], true ); } $plan = [ 'plan_name' => (string) $plan_name, 'plan_id' => (string) $plan_id, 'best_value' => $best_value, 'site_tiers' => $site_tiers_map, 'terms' => $terms, 'desc' => (string) $plan_desc, 'features' => $features, 'has_trial' => $has_trial, 'coupon' => (string) $plan_coupon, 'discount' => (string) $plan_discount, ]; if ( '' !== $plan['plan_id'] || '' !== $plan['plan_name'] ) { $plans[] = $plan; } } } return $plans; } // --- Rendering --- /** * Renders the parent pricing table with various configuration options. * * @param array $atts { * Array of attributes for configuring the pricing table. * - public_key (string): The public key used for Freemius integration. * - product_name (string): The name of the product being showcased. * - product_id (string): The unique identifier for the product. * - freemius (string): Defines Freemius mode, options are '', 'manual', or 'automatic'. * - buy_url (string): The URL used for purchasing the product when Freemius mode is empty. * - trial_url (string): The URL for a trial option when Freemius mode is empty. * - product_prefix (string): Prefix used to generate predictable button IDs. * - currency (string): The pricing currency for automatic mode. Default is 'usd'. * - cache_ttl (int): Cache duration (in seconds) for automatic mode API data. Default is 21600 (6 hours). * - site_tiers_label (string): Optional override for site tier labels in automatic mode. * - bearer_constant (string): The name of a constant storing the Freemius Bearer token for authentication. * }. * @param string|null $content Content passed to child shortcodes. Used in manual Freemius mode. * @return string The generated HTML for the pricing table or an error message if configuration is invalid. */ public function render_parent( $atts, $content = null ) { $a = shortcode_atts( [ 'public_key' => '', 'product_name' => '', 'product_id' => '', 'freemius' => '', // '', 'manual', 'automatic' 'buy_url' => '', // used when freemius is empty. requires full url including http(s):// 'trial_url' => '', // used when freemius is empty. requires full url including http(s):// 'product_prefix' => '', // used to generate predictable button IDs // Automatic mode optional controls: 'currency' => 'usd', // pricing currency for automatic mode 'cache_ttl' => '21600', // seconds (6h) for automatic mode API cache 'site_tiers_label' => '', // optional global labels override for automatic mode 'bearer_constant' => '', // name of a defined() constant that stores the Freemius Bearer token 'coupon' => '', // comma-separated Freemius coupon IDs (automatic mode only) 'discount' => '', // manual/empty mode: "$10" or "15%" global discount ], $atts, 'gopricingtable' ); $freemius_mode = strtolower( trim( (string) $a['freemius'] ) ); if ( in_array( $freemius_mode, [ 'manual', 'automatic' ], true ) ) { wp_enqueue_script( 'freemius-checkout', 'https://checkout.freemius.com/js/v1/', [], null, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion } $auto_product_name = ''; $auto_public_key = ''; $currency = 'usd'; $coupon_meta = [ 'codes_global' => [], 'codes_plan' => [], 'ids' => [], ]; if ( 'automatic' === $freemius_mode ) { $product_id = (string) $a['product_id']; if ( '' === trim( $product_id ) ) { return '