PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
← All changes | includes/admin/dashboard/class-charitable-dashboard.php +538 -29 1.8.11 → 1.8.13 View file →
@@ -58,8 +58,16 @@
58 58 */
59 59 const BLOG_POSTS_SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-blog-posts.json';
60 60
61 61 /**
62 + * URL for the v2 Latest Updates feed (hosted on Cloudflare Pages).
63 + *
64 + * @since 1.8.12
65 + * @var string
66 + */
67 + const LATEST_UPDATES_SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-latest-updates.json';
68 +
69 + /**
62 70 * Cached version information from the API.
63 71 *
64 72 * @since 1.8.8
65 73 *
@@ -593,41 +601,51 @@
593 601 * @param string $time_period The time period.
594 602 * @return array
595 603 */
596 604 public function get_date_range_for_period( $time_period ) {
597 - $end_date = gmdate( 'Y/m/d' );
605 + /*
606 + * Site-local, not UTC: this range feeds Charitable_Reports::get_data(),
607 + * which filters on p.post_date (site-local time), so a bound built from
608 + * bare UTC excludes part of today's donations on any site whose
609 + * timezone is ahead of UTC. Every case below is anchored to the same
610 + * site-local "now" so the range is internally consistent.
611 + *
612 + * @since 1.8.13
613 + */
614 + $now = current_time( 'timestamp', 0 );
615 + $end_date = gmdate( 'Y/m/d', $now );
598 616
599 617 switch ( $time_period ) {
600 618 case 'last-7-days':
601 - $start_date = gmdate( 'Y/m/d', strtotime( '-7 days' ) );
619 + $start_date = gmdate( 'Y/m/d', strtotime( '-7 days', $now ) );
602 620 $days = 7;
603 621 break;
604 622 case 'last-14-days':
605 - $start_date = gmdate( 'Y/m/d', strtotime( '-14 days' ) );
623 + $start_date = gmdate( 'Y/m/d', strtotime( '-14 days', $now ) );
606 624 $days = 14;
607 625 break;
608 626 case 'last-30-days':
609 - $start_date = gmdate( 'Y/m/d', strtotime( '-30 days' ) );
627 + $start_date = gmdate( 'Y/m/d', strtotime( '-30 days', $now ) );
610 628 $days = 30;
611 629 break;
612 630 case 'this-month':
613 - $start_date = gmdate( 'Y/m/d', strtotime( 'first day of this month' ) );
614 - $days = gmdate( 'j' ); // Current day of month
631 + $start_date = gmdate( 'Y/m/d', strtotime( 'first day of this month', $now ) );
632 + $days = gmdate( 'j', $now ); // Current day of month
615 633 break;
616 634 case 'last-3-months':
617 - $start_date = gmdate( 'Y/m/d', strtotime( '-3 months' ) );
635 + $start_date = gmdate( 'Y/m/d', strtotime( '-3 months', $now ) );
618 636 $days = 90;
619 637 break;
620 638 case 'last-6-months':
621 - $start_date = gmdate( 'Y/m/d', strtotime( '-6 months' ) );
639 + $start_date = gmdate( 'Y/m/d', strtotime( '-6 months', $now ) );
622 640 $days = 180;
623 641 break;
624 642 case 'last-year':
625 - $start_date = gmdate( 'Y/m/d', strtotime( '-1 year' ) );
643 + $start_date = gmdate( 'Y/m/d', strtotime( '-1 year', $now ) );
626 644 $days = 365;
627 645 break;
628 646 default:
629 - $start_date = gmdate( 'Y/m/d', strtotime( '-7 days' ) );
647 + $start_date = gmdate( 'Y/m/d', strtotime( '-7 days', $now ) );
630 648 $days = 7;
631 649 break;
632 650 }
633 651
@@ -856,10 +874,11 @@
856 874 $wpdb->esc_like( '_transient_timeout_charitable_dashboard_stats_changes_' ) . '%'
857 875 )
858 876 );
859 877
860 - // Clear blog posts cache
878 + // Clear blog posts cache and Latest Updates feed cache.
861 879 delete_transient( 'charitable_blog_posts_cache' );
880 + delete_transient( 'charitable_latest_updates_cache' );
862 881 }
863 882
864 883 /**
865 884 * Get the default tab from URL parameter.
@@ -1170,11 +1189,18 @@
1170 1189 }
1171 1190
1172 1191 // Get latest donations using Charitable_Reports
1173 1192 $charitable_reports = Charitable_Reports::get_instance();
1193 + /*
1194 + * Site-local, not UTC: post_date is stored in site-local time, so a
1195 + * bound built from bare UTC excludes part of today's donations on any
1196 + * site whose timezone is ahead of UTC.
1197 + *
1198 + * @since 1.8.13
1199 + */
1174 1200 $args = array(
1175 - 'start_date' => gmdate( 'Y/m/d', strtotime( '-30 days' ) ),
1176 - 'end_date' => gmdate( 'Y/m/d' ),
1201 + 'start_date' => gmdate( 'Y/m/d', current_time( 'timestamp', 0 ) - ( 30 * DAY_IN_SECONDS ) ),
1202 + 'end_date' => gmdate( 'Y/m/d', current_time( 'timestamp', 0 ) ),
1177 1203 'days' => 30,
1178 1204 );
1179 1205 $charitable_reports->init_with_array( 'dashboard', $args );
1180 1206 $donations = $charitable_reports->get_donations();
@@ -1941,8 +1967,100 @@
1941 1967 endforeach;
1942 1968 }
1943 1969
1944 1970 /**
1971 + * Render the "Free Site Analysis" promo card in the dashboard right column. Links to the
1972 + * Reports > Site Analysis tab, where the user runs their free Fundraising Score analysis.
1973 + *
1974 + * @since 1.8.12
1975 + *
1976 + * @return void
1977 + */
1978 + public function render_site_analysis_section() {
1979 + $charitable_sa_url = admin_url( 'admin.php?page=charitable-reports&tab=site-analysis' );
1980 + $charitable_sa_nonce = wp_create_nonce( 'charitable_site_analysis_click' );
1981 + ?>
1982 + <style>
1983 + /* This card puts content directly in section-content, which has no horizontal padding by
1984 + default; restore the standard section padding for this card only. */
1985 + #charitable-dashboard-v2-site-analysis .charitable-dashboard-v2-section-content { padding: 0 20px 20px; }
1986 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-new {
1987 + display: inline-block; margin-left: 8px; vertical-align: middle;
1988 + background: #5AA152; color: #fff; font-size: 10px; font-weight: 700;
1989 + line-height: 1; letter-spacing: .04em; text-transform: uppercase;
1990 + padding: 3px 7px; border-radius: 3px;
1991 + }
1992 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-body { display: flex; align-items: flex-start; gap: 14px; }
1993 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-icon {
1994 + flex: none; width: 40px; height: 40px; border-radius: 8px;
1995 + display: flex; align-items: center; justify-content: center; background: #eaf4e8; color: #5AA152;
1996 + }
1997 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-icon .dashicons { font-size: 22px; width: 22px; height: 22px; }
1998 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-text { margin: 0 0 12px; color: #50575e; font-size: 13.5px; line-height: 1.5; }
1999 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-button { background-color: #5AA152 !important; border-color: #5AA152 !important; color: #fff !important; text-shadow: none; }
2000 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-button:hover,
2001 + #charitable-dashboard-v2-site-analysis .charitable-sa-dash-button:focus { background-color: #3f7539 !important; border-color: #3f7539 !important; color: #fff !important; }
2002 + </style>
2003 +
2004 + <section id="charitable-dashboard-v2-site-analysis" class="charitable-dashboard-v2-section">
2005 + <header class="charitable-dashboard-v2-section-header">
2006 + <h3>
2007 + <?php esc_html_e( 'Free Site Analysis', 'charitable' ); ?>
2008 + <span class="charitable-sa-dash-new"><?php esc_html_e( 'New', 'charitable' ); ?></span>
2009 + </h3>
2010 + </header>
2011 + <div class="charitable-dashboard-v2-section-content">
2012 + <div class="charitable-sa-dash-body">
2013 + <span class="charitable-sa-dash-icon"><span class="dashicons dashicons-chart-area" aria-hidden="true"></span></span>
2014 + <div>
2015 + <p class="charitable-sa-dash-text">
2016 + <?php esc_html_e( 'See how your fundraising setup measures up and get specific, prioritized steps to raise more. Free to run, based on your current Charitable configuration.', 'charitable' ); ?>
2017 + </p>
2018 + <a href="<?php echo esc_url( $charitable_sa_url ); ?>" class="button button-primary charitable-sa-dash-button">
2019 + <?php esc_html_e( 'Run Your Free Analysis', 'charitable' ); ?>
2020 + </a>
2021 + </div>
2022 + </div>
2023 + </div>
2024 + </section>
2025 + <script>
2026 + /* Record the click via Charitable's consent-gated usage tracking (sendBeacon so it fires
2027 + before the link navigates away). No effect if the user hasn't opted into usage tracking. */
2028 + ( function () {
2029 + var link = document.querySelector( '#charitable-dashboard-v2-site-analysis .charitable-sa-dash-button' );
2030 + if ( ! link || ! navigator.sendBeacon ) { return; }
2031 + link.addEventListener( 'click', function () {
2032 + try {
2033 + var fd = new FormData();
2034 + fd.append( 'action', 'charitable_track_site_analysis_click' );
2035 + fd.append( 'nonce', '<?php echo esc_js( $charitable_sa_nonce ); ?>' );
2036 + navigator.sendBeacon( '<?php echo esc_url_raw( admin_url( 'admin-ajax.php' ) ); ?>', fd );
2037 + } catch ( e ) {}
2038 + } );
2039 + } )();
2040 + </script>
2041 + <?php
2042 + }
2043 +
2044 + /**
2045 + * AJAX: record a click on the dashboard "Run Your Free Analysis" button via the same
2046 + * consent-gated usage-tracking channel as the recommendation cards. Lands in the
2047 + * charitable_recommended_plugins_clicked option, which Charitable_Tracking reports to
2048 + * awesomemotive as recommended_plugins_clicked (filter by context 'dashboard_site_analysis').
2049 + *
2050 + * @since 1.8.12
2051 + *
2052 + * @return void
2053 + */
2054 + public function ajax_track_site_analysis_click() {
2055 + if ( ! check_ajax_referer( 'charitable_site_analysis_click', 'nonce', false ) || ! current_user_can( 'manage_charitable_settings' ) ) {
2056 + wp_send_json_error();
2057 + }
2058 + $this->track_recommendation_click( 'clicked', 'site-analysis', 'feature', 'dashboard_site_analysis' );
2059 + wp_send_json_success();
2060 + }
2061 +
2062 + /**
1945 2063 * Render the enhance campaign section.
1946 2064 *
1947 2065 * @since 1.8.8
1948 2066 */
@@ -2935,16 +3053,46 @@
2935 3053 *
2936 3054 * @since 1.8.8
2937 3055 */
2938 3056 public function render_latest_updates_section() {
2939 - // Check if there are blog posts to determine footer link text
2940 - $posts = $this->get_blog_posts();
2941 - $has_posts = ! empty( $posts ) && ! $this->has_blog_posts_error( $posts );
2942 - $footer_link_text = $has_posts ? __( 'Read More', 'charitable' ) : __( 'Read Blog', 'charitable' );
3057 + // Try v2 feed first, fall back to legacy blog posts.
3058 + $updates = $this->get_latest_updates();
3059 + $has_posts = ! empty( $updates );
3060 + if ( ! $has_posts ) {
3061 + $posts = $this->get_blog_posts();
3062 + $has_posts = ! empty( $posts ) && ! $this->has_blog_posts_error( $posts );
3063 + }
3064 + $footer_link_text = __( 'See More Updates', 'charitable' );
2943 3065
2944 - // Check if user is more than 3 updates behind
3066 + // Threshold resolves in order: feed meta.behind_threshold → WP filter → hardcoded default (3).
2945 3067 $updates_behind = $this->get_updates_behind();
2946 - $show_version_update = $updates_behind > 3;
3068 + $threshold = 3;
3069 + // isset (not ! empty) so a feed-supplied 0 correctly overrides to "always alert".
3070 + if ( isset( $this->version_info['behind_threshold'] ) ) {
3071 + $threshold = (int) $this->version_info['behind_threshold'];
3072 + }
3073 + /**
3074 + * Filter the "behind versions" threshold above which the big update alert renders.
3075 + *
3076 + * @since 1.8.12
3077 + *
3078 + * @param int $threshold Default 3, or value supplied by the feed.
3079 + */
3080 + $threshold = (int) apply_filters( 'charitable_latest_updates_behind_threshold', $threshold );
3081 + $show_version_update = $updates_behind > $threshold;
3082 +
3083 + // Version-state pill resolution (mutually exclusive with the big alert). @since 1.8.12
3084 + // - on_latest : remote latest is known AND user is 0 behind.
3085 + // - mildly_behind : 1+ behind but not past the threshold (shows a compact amber nudge).
3086 + $latest_known_version = $this->get_latest_version();
3087 + $on_latest = ! empty( $latest_known_version ) && 0 === $updates_behind;
3088 + $mildly_behind = ! empty( $latest_known_version ) && $updates_behind > 0 && ! $show_version_update;
3089 +
3090 + // If blog posts can't load and there's no version update to show, don't display the section at all
3091 + if ( ! $has_posts && ! $show_version_update ) {
3092 + return;
3093 + }
3094 +
2947 3095 $section_class = 'charitable-dashboard-v2-section';
2948 3096 if ( ! $show_version_update ) {
2949 3097 $section_class .= ' no-update-alert';
2950 3098 }
@@ -2952,8 +3100,49 @@
2952 3100 <section id="charitable-dashboard-v2-latest-updates" class="<?php echo esc_attr( $section_class ); ?>">
2953 3101 <header class="charitable-dashboard-v2-section-header">
2954 3102 <h3>Latest Updates</h3>
2955 3103 </header>
3104 + <?php if ( $on_latest ) : ?>
3105 + <div class="charitable-dashboard-v2-latest-updates-uptodate">
3106 + <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14" aria-hidden="true">
3107 + <path d="M16.7 5.3L7.5 14.6L3.3 10.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
3108 + </svg>
3109 + <span><?php
3110 + echo wp_kses(
3111 + sprintf(
3112 + /* translators: %s: current installed version number */
3113 + __( "You're running the latest version (<strong>%s</strong>).", 'charitable' ),
3114 + esc_html( charitable()->get_version() )
3115 + ),
3116 + array( 'strong' => array() )
3117 + );
3118 + ?></span>
3119 + </div>
3120 + <?php elseif ( $mildly_behind ) : ?>
3121 + <div class="charitable-dashboard-v2-latest-updates-behind">
3122 + <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14" aria-hidden="true">
3123 + <circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/>
3124 + <path d="M10 9v4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
3125 + <circle cx="10" cy="6.3" r="1.1" fill="currentColor"/>
3126 + </svg>
3127 + <span><?php
3128 + $versions_label = sprintf(
3129 + /* translators: %d: number of versions behind */
3130 + _n( '%d version', '%d versions', $updates_behind, 'charitable' ),
3131 + $updates_behind
3132 + );
3133 + echo wp_kses(
3134 + sprintf(
3135 + /* translators: 1: versions-behind label (e.g. "2 versions"), 2: plugins page URL */
3136 + __( 'You are behind <strong>%1$s</strong>. <a href="%2$s">Update now</a>', 'charitable' ),
3137 + esc_html( $versions_label ),
3138 + esc_url( admin_url( 'plugins.php?s=charitable' ) )
3139 + ),
3140 + array( 'strong' => array(), 'a' => array( 'href' => array() ) )
3141 + );
3142 + ?></span>
3143 + </div>
3144 + <?php endif; ?>
2956 3145 <div class="charitable-dashboard-v2-section-content">
2957 3146 <?php if ( $show_version_update ) : ?>
2958 3147 <div class="charitable-dashboard-v2-latest-updates-background">
2959 3148 <div class="charitable-dashboard-v2-latest-updates-background-content">
@@ -2959,29 +3148,37 @@
2959 3148 <div class="charitable-dashboard-v2-latest-updates-background-content">
2960 3149 <div class="charitable-dashboard-v2-latest-updates-flex">
2961 3150 <div class="charitable-dashboard-v2-latest-updates-icon">
2962 3151 <svg class="charitable-dashboard-v2-latest-updates-icon-placeholder" width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
2963 - <circle cx="13" cy="13" r="13" fill="#eaa251"/>
2964 - <path d="M13 6.5L19.5 17.5H6.5L13 6.5Z" fill="white"/>
2965 - <path d="M13 10.5V14.5" stroke="#eaa251" stroke-width="2" stroke-linecap="round"/>
2966 - <circle cx="13" cy="17" r="1" fill="#eaa251"/>
3152 + <path d="M13 2L24 22H2L13 2Z" stroke="#fff" stroke-width="2" stroke-linejoin="round"/>
3153 + <path d="M13 10V16" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
3154 + <circle cx="13" cy="19.5" r="1.25" fill="#fff"/>
2967 3155 </svg>
2968 3156 </div>
2969 3157 <div class="charitable-dashboard-v2-latest-updates-content">
2970 - <h4 class="charitable-dashboard-v2-latest-updates-headline">You are currently <?php echo esc_html( $updates_behind ); ?> updates behind. Please upgrade Charitable to take advantage of:</h4>
2971 - <div class="charitable-dashboard-v2-latest-updates-features">
2972 - <?php $this->render_update_features(); ?>
2973 - </div>
3158 + <h4 class="charitable-dashboard-v2-latest-updates-headline"><?php
3159 + $version_label = sprintf( '<strong>%s</strong>', esc_html( sprintf( _n( '%d version', '%d versions', $updates_behind, 'charitable' ), $updates_behind ) ) );
3160 + $update_link = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'plugins.php?s=charitable' ) ), esc_html__( 'Update now', 'charitable' ) );
3161 + printf(
3162 + wp_kses(
3163 + /* translators: 1: versions-behind label (e.g. "4 versions"), 2: "Update now" link */
3164 + __( 'You are currently %1$s behind. Please upgrade Charitable to take advantage of the features and addons below. %2$s', 'charitable' ),
3165 + array( 'strong' => array(), 'a' => array( 'href' => array() ) )
3166 + ),
3167 + $version_label,
3168 + $update_link
3169 + );
3170 + ?></h4>
2974 3171 </div>
2975 3172 </div>
2976 3173 </div>
2977 3174 </div>
2978 3175 <?php endif; ?>
2979 - <?php $this->render_blog_posts(); ?>
3176 + <?php $this->render_latest_updates_items(); ?>
2980 3177 <footer class="charitable-dashboard-v2-latest-updates-footer">
2981 3178 <div class="charitable-dashboard-v2-latest-updates-footer-content">
2982 3179 <div class="charitable-dashboard-v2-latest-updates-footer-left">
2983 - <a href="https://wpcharitable.com/blog" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-link">
3180 + <a href="https://www.wpcharitable.com/charitable-changelog/" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-link">
2984 3181 <span class="charitable-dashboard-v2-blog-text"><?php echo esc_html( $footer_link_text ); ?></span>
2985 3182 <svg class="charitable-dashboard-v2-arrow-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2986 3183 <path d="M6 12L10 8L6 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2987 3184 </svg>
@@ -2990,8 +3187,320 @@
2990 3187 </div>
2991 3188 </footer>
2992 3189 </div>
2993 3190 </section>
3191 + <?php
3192 + }
3193 +
3194 +
3195 + /**
3196 + * Render the "Are you enjoying Charitable?" section on the dashboard.
3197 + * Mirrors the top admin notice (five-star review) but styled to match the dashboard.
3198 + * Only shows when the same eligibility conditions are met and it hasn't been dismissed.
3199 + *
3200 + * @since 1.8.12
3201 + */
3202 + public function render_enjoying_section() {
3203 + // ?preview_enjoy=1 bypasses all eligibility checks for testing (admins only).
3204 + $preview = ! empty( $_GET['preview_enjoy'] ) && current_user_can( 'manage_options' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
3205 +
3206 + if ( ! $preview ) {
3207 + $activated_datetime = get_option( 'wpcharitable_activated_datetime', false );
3208 + $days = 0;
3209 + if ( $activated_datetime ) {
3210 + $diff = current_time( 'timestamp' ) - $activated_datetime;
3211 + $days = (int) abs( round( $diff / 86400 ) );
3212 + }
3213 +
3214 + $count_campaigns = wp_count_posts( 'campaign' );
3215 + $total_campaigns = isset( $count_campaigns->publish ) ? (int) $count_campaigns->publish : 0;
3216 + $count_donations = wp_count_posts( 'donation' );
3217 + $total_donations = isset( $count_donations->{'charitable-completed'} ) ? (int) $count_donations->{'charitable-completed'} : 0;
3218 +
3219 + if ( $days < apply_filters( 'charitable_days_since_activated', 14 ) || $total_campaigns < 1 || $total_donations < 1 ) {
3220 + return;
3221 + }
3222 +
3223 + // Use the same transient as the top notice so dismissing either dismisses both.
3224 + if ( get_transient( 'charitable_five-star-review_banner' ) ) {
3225 + return;
3226 + }
3227 + }
3228 +
3229 + $feedback_url = 'https://wpcharitable.com/charitable-admin-review/';
3230 + $rating_url = 'https://wpcharitable.com/charitable-admin-review/';
3231 + ?>
3232 + <section id="charitable-dashboard-enjoy-section" class="charitable-dashboard-v2-section charitable-dashboard-enjoy-section">
3233 + <button type="button" class="charitable-enjoy-dismiss-btn" aria-label="<?php esc_attr_e( 'Dismiss', 'charitable' ); ?>">
3234 + <svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M1 1L11 11M11 1L1 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
3235 + </button>
3236 +
3237 + <div class="charitable-enjoy-step" data-step="1">
3238 + <p class="charitable-enjoy-question"><?php esc_html_e( 'Are You Enjoying Charitable?', 'charitable' ); ?></p>
3239 + <p class="charitable-enjoy-actions">
3240 + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--yes" data-navigate="3"><?php esc_html_e( 'Yes', 'charitable' ); ?></button>
3241 + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--no" data-navigate="2"><?php esc_html_e( 'Not Really', 'charitable' ); ?></button>
3242 + </p>
3243 + </div>
3244 +
3245 + <div class="charitable-enjoy-step" data-step="2" style="display:none;">
3246 + <p class="charitable-enjoy-question charitable-enjoy-question--title"><?php esc_html_e( "Sorry To Hear That!", 'charitable' ); ?></p>
3247 + <p class="charitable-enjoy-question"><?php esc_html_e( "We're sorry to hear you aren't enjoying Charitable. We would love a chance to improve. Could you take a minute and let us know what we can do better?", 'charitable' ); ?></p>
3248 + <p class="charitable-enjoy-actions">
3249 + <a href="<?php echo esc_url( $feedback_url ); ?>" class="charitable-enjoy-link charitable-enjoy-link--yes charitable-enjoy-dismiss-action" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Give Feedback', 'charitable' ); ?></a>
3250 + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--no charitable-enjoy-dismiss-action"><?php esc_html_e( 'No Thanks', 'charitable' ); ?></button>
3251 + </p>
3252 + </div>
3253 +
3254 + <div class="charitable-enjoy-step" data-step="3" style="display:none;">
3255 + <p class="charitable-enjoy-question charitable-enjoy-question--title"><?php esc_html_e( "That's Awesome!", 'charitable' ); ?></p>
3256 + <p class="charitable-enjoy-question"><?php esc_html_e( "Could you please do us a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our team motivation?", 'charitable' ); ?></p>
3257 + <p class="charitable-enjoy-team"><strong><?php esc_html_e( '~ WP Charitable Team', 'charitable' ); ?></strong></p>
3258 + <p class="charitable-enjoy-actions">
3259 + <a href="<?php echo esc_url( $rating_url ); ?>" class="charitable-enjoy-link charitable-enjoy-link--yes charitable-enjoy-dismiss-action" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Ok, You Deserve It', 'charitable' ); ?></a>
3260 + <button type="button" class="charitable-enjoy-link charitable-enjoy-link--no charitable-enjoy-dismiss-action"><?php esc_html_e( 'I Already Did', 'charitable' ); ?></button>
3261 + </p>
3262 + </div>
3263 + </section>
3264 + <script>
3265 + ( function ( $ ) {
3266 + var section = $( '#charitable-dashboard-enjoy-section' );
3267 +
3268 + // Navigate between steps.
3269 + section.on( 'click', '[data-navigate]', function () {
3270 + var target = $( this ).data( 'navigate' );
3271 + section.find( '.charitable-enjoy-step' ).hide();
3272 + section.find( '.charitable-enjoy-step[data-step="' + target + '"]' ).show();
3273 + } );
3274 +
3275 + // Dismiss — fires AJAX then removes the section.
3276 + function dismissSection() {
3277 + $.post( ajaxurl, {
3278 + action: 'charitable_dismiss_admin_five_star_rating',
3279 + banner_id: 'five-star-review',
3280 + } );
3281 + section.fadeOut( 300, function () { $( this ).remove(); } );
3282 + }
3283 +
3284 + section.on( 'click', '.charitable-enjoy-dismiss-btn', dismissSection );
3285 + section.on( 'click', '.charitable-enjoy-dismiss-action', function () {
3286 + setTimeout( dismissSection, 150 );
3287 + } );
3288 + }( jQuery ) );
3289 + </script>
3290 + <?php
3291 + }
3292 + /**
3293 + * Map a URL to a local filesystem path if it lives under WP_CONTENT_URL.
3294 + *
3295 + * @param string $url URL to check.
3296 + * @return string|false Absolute file path, or false if the URL is external.
3297 + */
3298 + private function get_local_file_path_for_url( $url ) {
3299 + $content_url = untrailingslashit( content_url() );
3300 + // Normalize schemes so http/https mismatch doesn't break the comparison.
3301 + $url_norm = preg_replace( '#^https?://#', '//', $url );
3302 + $content_norm = preg_replace( '#^https?://#', '//', $content_url );
3303 + if ( strpos( $url_norm, $content_norm ) !== 0 ) {
3304 + return false;
3305 + }
3306 + $relative = substr( $url_norm, strlen( $content_norm ) );
3307 + $file = WP_CONTENT_DIR . $relative;
3308 + return file_exists( $file ) ? $file : false;
3309 + }
3310 + /**
3311 + * Return a local video URL for a given post title, if one exists.
3312 + * Used to override missing video_url values from the remote feed.
3313 + *
3314 + * @param string $title Post title from the feed.
3315 + * @return string URL or empty string.
3316 + */
3317 + private function get_local_video_url( $title ) {
3318 + $map = array(
3319 + 'Mini Donation Widget' => site_url( '/_promo_videos/charitable_dashboard_updates_video_mini-widget.mp4' ),
3320 + );
3321 +
3322 + foreach ( $map as $keyword => $url ) {
3323 + if ( false !== stripos( $title, $keyword ) ) {
3324 + return $url;
3325 + }
3326 + }
3327 +
3328 + return '';
3329 + }
3330 + /**
3331 + * Return a local thumbnail URL for a given post title, if one exists.
3332 + *
3333 + * @param string $title Post title from the feed.
3334 + * @return string URL or empty string.
3335 + */
3336 + private function get_local_thumbnail_url( $title ) {
3337 + $base = charitable()->get_path( 'assets', false ) . 'images/dashboard/';
3338 + $map = array(
3339 + 'Mini Donation Widget' => site_url( '/_promo_videos/mini-widget-thumbnail.svg' ),
3340 + 'Donations Feed' => site_url( '/_promo_videos/donations-feed-thumbnail.svg' ),
3341 + 'Campaign Showcase' => $base . 'thumb-campaign-showcase.png',
3342 + 'Campaign Updates' => $base . 'thumb-campaign-updates.svg',
3343 + );
3344 +
3345 + foreach ( $map as $keyword => $url ) {
3346 + if ( false !== stripos( $title, $keyword ) ) {
3347 + return $url;
3348 + }
3349 + }
3350 +
3351 + return '';
3352 + }
3353 + /**
3354 + * Return a local badge type for a given post title, if one should be overridden.
3355 + * Values: 'pro', 'addon', or empty string.
3356 + *
3357 + * @param string $title Post title from the feed.
3358 + * @return string Badge type or empty string.
3359 + */
3360 + private function get_local_badge( $title ) {
3361 + $pro_keywords = array( 'Mini Donation Widget', 'Donations Feed', 'Campaign Showcase' );
3362 + foreach ( $pro_keywords as $keyword ) {
3363 + if ( false !== stripos( $title, $keyword ) ) {
3364 + return 'pro';
3365 + }
3366 + }
3367 +
3368 + $addon_keywords = array( 'Campaign Updates Addon', 'Campaigns Update Addon' );
3369 + foreach ( $addon_keywords as $keyword ) {
3370 + if ( false !== stripos( $title, $keyword ) ) {
3371 + return 'addon';
3372 + }
3373 + }
3374 +
3375 + return '';
3376 + }
3377 + /**
3378 + * Fetch and return the v2 Latest Updates feed.
3379 + *
3380 + * @since 1.8.12
3381 + * @return array Array of update items, or empty array on failure.
3382 + */
3383 + private function get_latest_updates() {
3384 + $cached = get_transient( 'charitable_latest_updates_cache' );
3385 + if ( false !== $cached ) {
3386 + if ( isset( $cached['version_info'] ) ) {
3387 + $this->version_info = $cached['version_info'];
3388 + }
3389 + return $cached['updates'] ?? [];
3390 + }
3391 +
3392 + $response = wp_remote_get( self::LATEST_UPDATES_SOURCE_URL, [
3393 + 'timeout' => 10,
3394 + 'sslverify' => false,
3395 + ] );
3396 +
3397 + if ( is_wp_error( $response ) ) {
3398 + return [];
3399 + }
3400 +
3401 + $body = wp_remote_retrieve_body( $response );
3402 + if ( empty( $body ) ) {
3403 + return [];
3404 + }
3405 +
3406 + $decoded = json_decode( $body, true );
3407 + if ( json_last_error() !== JSON_ERROR_NONE || empty( $decoded['updates'] ) ) {
3408 + return [];
3409 + }
3410 +
3411 + // Store version info from meta.
3412 + if ( isset( $decoded['meta'] ) && is_array( $decoded['meta'] ) ) {
3413 + $this->version_info = [
3414 + 'current_charitable_version_lite' => ! empty( $decoded['meta']['current_charitable_version_lite'] ) ? sanitize_text_field( $decoded['meta']['current_charitable_version_lite'] ) : '',
3415 + 'current_charitable_version_pro' => ! empty( $decoded['meta']['current_charitable_version_pro'] ) ? sanitize_text_field( $decoded['meta']['current_charitable_version_pro'] ) : '',
3416 + ];
3417 + }
3418 +
3419 + // Sanitize each update item.
3420 + $updates = [];
3421 + foreach ( $decoded['updates'] as $item ) {
3422 + if ( empty( $item['title'] ) || empty( $item['url'] ) ) {
3423 + continue;
3424 + }
3425 + $updates[] = [
3426 + 'id' => sanitize_key( $item['id'] ?? '' ),
3427 + 'title' => sanitize_text_field( $item['title'] ),
3428 + 'description' => sanitize_text_field( $item['description'] ?? '' ),
3429 + 'url' => esc_url_raw( $item['url'] ),
3430 + 'date' => sanitize_text_field( $item['date'] ?? '' ),
3431 + 'badge' => sanitize_key( $item['badge'] ?? '' ),
3432 + 'is_new' => ! empty( $item['is_new'] ),
3433 + 'image' => ! empty( $item['image'] ) ? esc_url_raw( $item['image'] ) : '',
3434 + ];
3435 + }
3436 +
3437 + // Cache for 24 hours.
3438 + set_transient( 'charitable_latest_updates_cache', [
3439 + 'updates' => $updates,
3440 + 'version_info' => $this->version_info,
3441 + ], DAY_IN_SECONDS );
3442 +
3443 + return $updates;
3444 + }
3445 + /**
3446 + * Render the v2 Latest Updates items.
3447 + *
3448 + * @since 1.8.12
3449 + */
3450 + private function render_latest_updates_items() {
3451 + $updates = $this->get_latest_updates();
3452 +
3453 + if ( empty( $updates ) ) {
3454 + // Fall back to legacy blog posts.
3455 + $this->render_blog_posts();
3456 + return;
3457 + }
3458 +
3459 + ?>
3460 + <div class="charitable-dashboard-v2-blog-posts" id="charitable-blog-posts-container">
3461 + <?php $count = 0; foreach ( $updates as $update ) : if ( $count >= 4 ) break; $count++; ?>
3462 + <div class="charitable-dashboard-v2-blog-post-row">
3463 + <div class="charitable-dashboard-v2-blog-post-left">
3464 + <div class="charitable-dashboard-v2-blog-post-timestamp-row">
3465 + <?php if ( ! empty( $update['date'] ) ) : ?>
3466 + <span class="charitable-dashboard-v2-blog-post-timestamp"><?php echo esc_html( $this->calculate_relative_time( $update['date'] ) ); ?></span>
3467 + <?php endif; ?>
3468 + <?php if ( ! empty( $update['is_new'] ) ) : ?>
3469 + <span class="charitable-dashboard-v2-blog-post-badge-new"><?php esc_html_e( 'New', 'charitable' ); ?></span>
3470 + <?php endif; ?>
3471 + <?php if ( 'pro' === $update['badge'] ) : ?>
3472 + <span class="charitable-dashboard-v2-blog-post-badge-pro"><?php esc_html_e( 'Pro', 'charitable' ); ?></span>
3473 + <?php elseif ( 'addon' === $update['badge'] ) : ?>
3474 + <span class="charitable-dashboard-v2-blog-post-badge-addon"><?php esc_html_e( 'Addon', 'charitable' ); ?></span>
3475 + <?php endif; ?>
3476 + </div>
3477 + <h5 class="charitable-dashboard-v2-blog-post-title">
3478 + <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $update['title'] ); ?></a>
3479 + </h5>
3480 + <?php if ( ! empty( $update['description'] ) ) : ?>
3481 + <p class="charitable-dashboard-v2-blog-post-desc"><?php echo esc_html( $update['description'] ); ?> <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-post-learn-more"><?php esc_html_e( 'Learn More', 'charitable' ); ?> →</a></p>
3482 + <?php endif; ?>
3483 + </div>
3484 + <div class="charitable-dashboard-v2-blog-post-right">
3485 + <?php if ( ! empty( $update['image'] ) ) : ?>
3486 + <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-post-thumb charitable-dashboard-v2-blog-post-thumb--bg" style="background-image:url('<?php echo esc_url( $update['image'] ); ?>');">
3487 + </a>
3488 + <?php else : ?>
3489 + <a href="<?php echo esc_url( $update['url'] ); ?>" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-blog-post-thumb">
3490 + <svg width="96" height="64" viewBox="0 0 96 64" fill="none" xmlns="http://www.w3.org/2000/svg">
3491 + <rect width="96" height="64" rx="6" fill="#E5E7EB"/>
3492 + <path d="M16 46L30 29L46 39L62 21L82 33" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
3493 + <circle cx="30" cy="29" r="2.5" fill="#9CA3AF"/>
3494 + <circle cx="46" cy="39" r="2.5" fill="#9CA3AF"/>
3495 + <circle cx="62" cy="21" r="2.5" fill="#9CA3AF"/>
3496 + </svg>
3497 + </a>
3498 + <?php endif; ?>
3499 + </div>
3500 + </div>
3501 + <?php endforeach; ?>
3502 + </div>
2994 3503 <?php
2995 3504 }
2996 3505
2997 3506 /**