Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/charitable/charitable.php
+++ b/charitable/charitable.php
@@ -3,11 +3,11 @@
* Plugin Name: Charitable
* Plugin URI: https://www.wpcharitable.com
* Description: The best WordPress donation plugin. Fundraising with recurring donations, and powerful features to help you raise more money online.
- * Version: 1.8.10.4
+ * Version: 1.8.10.5
* Author: Charitable Donations & Fundraising Team
* Author URI: https://wpcharitable.com
* Requires at least: 5.0
- * Stable tag: 1.8.10.4
+ * Stable tag: 1.8.10.5
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
@@ -39,7 +39,7 @@
const AUTHOR = 'WP Charitable';
/* Plugin version. */
- const VERSION = '1.8.10.4';
+ const VERSION = '1.8.10.5';
/* Version of database schema. */
const DB_VERSION = '20180522';
--- a/charitable/includes/abstracts/abstract-class-charitable-form.php
+++ b/charitable/includes/abstracts/abstract-class-charitable-form.php
@@ -632,7 +632,7 @@
'Charitable_Public_Form_View::get_template_name()'
);
- return $form->view()->get_template_name( $field );
+ return $this->view()->get_template_name( $field );
}
/**
--- a/charitable/includes/admin/addons-directory/class-charitable-addons-directory.php
+++ b/charitable/includes/admin/addons-directory/class-charitable-addons-directory.php
@@ -479,27 +479,114 @@
// phpcs:enable
/*
- * Remove addons that are not needed for pro.
+ * Pro addon card visibility in the Charitable → Addons screen.
+ *
+ * Default: both `charitable-pro-plugin` and `charitable-pro` slugs are hidden
+ * so the normal upgrade path (wpcharitable.com checkout → Plugins → Add New
+ * upload) is the only way to install Pro.
+ *
+ * With CHARITABLE_SHOW_PRO_IN_ADDONS defined truthy (wp-config.php or a
+ * code-snippet plugin), the Pro card is exposed so Pro can be installed
+ * server-to-server from this screen — unblocking managed-host customers
+ * (WP.com, Kinsta, WP Engine, SiteGround) whose manual-upload path rejects
+ * the Pro zip with `http_error`.
+ *
+ * The wpcharitable.com addons API currently returns Pro under the legacy slug
+ * `charitable-pro-plugin`, while on disk Pro lives at `charitable-pro/
+ * charitable-pro.php`. If we rendered the raw entry, install-state detection,
+ * basename lookup, and activation would all use the wrong path and the flow
+ * would fail at activation. So when we expose Pro we NORMALISE the entry in
+ * place: rewrite its slug to `charitable-pro` and its path to match the
+ * on-disk location.
+ *
+ * Wrapped in try/catch so that any unexpected payload shape cannot fatal the
+ * admin Addons screen. On failure we fall back to the safe pre-1.8.10.5
+ * behaviour of hiding both Pro slugs.
*
* @since 1.8.5
*/
- $addons_to_remove = array(
- 'charitable-pro-plugin',
- 'charitable-pro',
- );
+ try {
+ $show_pro = defined( 'CHARITABLE_SHOW_PRO_IN_ADDONS' ) && CHARITABLE_SHOW_PRO_IN_ADDONS;
+
+ foreach ( $addon_categories as $slug => &$category ) {
+ if ( ! is_array( $category ) || ! isset( $category['addons'] ) || ! is_array( $category['addons'] ) ) {
+ continue;
+ }
+ foreach ( $category['addons'] as $k => &$v ) {
+ if ( ! is_array( $v ) || ! isset( $v['slug'] ) ) {
+ continue;
+ }
+
+ $is_pro_legacy = 'charitable-pro-plugin' === $v['slug'];
+ $is_pro_canonical = 'charitable-pro' === $v['slug'];
+
+ if ( ! $is_pro_legacy && ! $is_pro_canonical ) {
+ continue;
+ }
+
+ if ( ! $show_pro ) {
+ unset( $category['addons'][ $k ] );
+ continue;
+ }
+
+ // Normalise the legacy slug onto the canonical on-disk layout so
+ // install, activation, and "is installed" detection all line up.
+ if ( $is_pro_legacy ) {
+ $v['slug'] = 'charitable-pro';
+ $v['path'] = 'charitable-pro/charitable-pro.php';
+ }
+ }
+ // Reindex array after any removal.
+ $category['addons'] = array_values( $category['addons'] );
+ }
+ unset( $category ); // Break the reference.
+ unset( $v ); // Break the reference.
- // Recursively remove specified addons.
- foreach ( $addon_categories as $slug => &$category ) {
- foreach ( $category['addons'] as $k => &$v ) {
- if ( is_array( $v ) && isset( $v['slug'] ) && in_array( $v['slug'], $addons_to_remove ) ) {
- unset( $category['addons'][ $k ] );
+ // Defensive dedup: if both the legacy and canonical slugs were present
+ // server-side, the rename above could produce two `charitable-pro`
+ // entries. Keep only the first across all categories.
+ if ( $show_pro ) {
+ $seen_pro = false;
+ foreach ( $addon_categories as $slug => &$category ) {
+ if ( ! is_array( $category ) || ! isset( $category['addons'] ) || ! is_array( $category['addons'] ) ) {
+ continue;
+ }
+ foreach ( $category['addons'] as $k => &$v ) {
+ if ( is_array( $v ) && isset( $v['slug'] ) && 'charitable-pro' === $v['slug'] ) {
+ if ( $seen_pro ) {
+ unset( $category['addons'][ $k ] );
+ } else {
+ $seen_pro = true;
+ }
+ }
+ }
+ $category['addons'] = array_values( $category['addons'] );
}
+ unset( $category );
+ unset( $v );
+ }
+ } catch ( Throwable $e ) {
+ // Fall back to the pre-1.8.10.5 behaviour: always hide both Pro slugs.
+ $addons_to_remove = array( 'charitable-pro-plugin', 'charitable-pro' );
+ if ( is_array( $addon_categories ) ) {
+ foreach ( $addon_categories as $slug => &$category ) {
+ if ( ! is_array( $category ) || ! isset( $category['addons'] ) || ! is_array( $category['addons'] ) ) {
+ continue;
+ }
+ foreach ( $category['addons'] as $k => &$v ) {
+ if ( is_array( $v ) && isset( $v['slug'] ) && in_array( $v['slug'], $addons_to_remove, true ) ) {
+ unset( $category['addons'][ $k ] );
+ }
+ }
+ $category['addons'] = array_values( $category['addons'] );
+ }
+ unset( $category );
+ unset( $v );
+ }
+ if ( function_exists( 'charitable_is_debug' ) && charitable_is_debug() ) {
+ error_log( 'CHARITABLE: Pro addon card gating failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
- // Reindex array after removal.
- $category['addons'] = array_values( $category['addons'] );
}
- unset( $category ); // Break the reference.
- unset( $v ); // Break the reference.
?>
<div id="charitable-addons">
@@ -723,6 +810,35 @@
$addon['action'] = 'install';
}
}
+
+ /*
+ * Charitable Pro is available to every legitimate license tier (Basic,
+ * Plus, Pro, Agency). The tier-match loop above would otherwise show
+ * "Upgrade Now" to Basic/Plus customers because Pro's API license field
+ * typically only lists `pro`. Override to "install" when the current
+ * site has any valid license AND the API actually returned a usable
+ * download_link — without a download_link the install step would fail,
+ * so we fall back to the upgrade CTA in that case.
+ *
+ * Wrapped in try/catch so that a failure in the licence helper chain
+ * (registry / vendor licence lookup) cannot fatal an admin page render.
+ * On failure the card simply keeps whatever action the tier-match loop
+ * above assigned — i.e. the original pre-1.8.10.5 behaviour.
+ */
+ try {
+ if ( 'charitable-pro' === $addon['slug']
+ && 'install' !== $addon['action']
+ && function_exists( 'charitable_is_pro' )
+ && charitable_is_pro()
+ && ! empty( $addon['download_link'] ) ) {
+ $addon['status'] = 'missing';
+ $addon['action'] = 'install';
+ }
+ } catch ( Throwable $e ) {
+ if ( function_exists( 'charitable_is_debug' ) && charitable_is_debug() ) {
+ error_log( 'CHARITABLE: Pro install-action override failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ }
+ }
} else { // phpcs:ignore
// Plugin is installed.
if ( $addon['is_active'] ) {
@@ -763,22 +879,55 @@
$status_label = $this->get_addon_status_label( $addon['status'] );
- // get the icon/graphic.
- $icon = isset( $addon['icon'] ) ? esc_url( $addon['icon'] ) : 'placeholder';
-
- $icon_file_name = str_replace( 'charitable-', '', $addon['slug'] );
- $icon_file_name = str_replace( '-banner', '', $icon_file_name );
-
- // Locate icon, if it exists.
- if ( ! file_exists( charitable()->get_path( 'assets', true ) . 'images/addons/addon-icon-' . $icon_file_name . '.png' ) ) {
- $icon = charitable()->get_path( 'assets', false ) . 'images/addons/addon-icon-stripe.png';
+ // Resolve the icon. Lookup order:
+ // 1. Local SVG (assets/images/addons/addon-icon-{slug}.svg)
+ // 2. Local PNG (assets/images/addons/addon-icon-{slug}.png)
+ // 3. API-supplied icon URL (so newly added addons show the right art
+ // even before a local asset ships in a Lite release)
+ // 4. Stripe placeholder (last resort so the card never renders broken)
+ $icon_file_name = str_replace( array( 'charitable-', '-banner' ), '', $addon['slug'] );
+ $assets_path_fs = charitable()->get_path( 'assets', true ) . 'images/addons/';
+ $assets_path_url = charitable()->get_path( 'assets', false ) . 'images/addons/';
+ $placeholder_icon = $assets_path_url . 'addon-icon-stripe.png';
+
+ if ( file_exists( $assets_path_fs . 'addon-icon-' . $icon_file_name . '.svg' ) ) {
+ $icon = $assets_path_url . 'addon-icon-' . $icon_file_name . '.svg';
+ } elseif ( file_exists( $assets_path_fs . 'addon-icon-' . $icon_file_name . '.png' ) ) {
+ $icon = $assets_path_url . 'addon-icon-' . $icon_file_name . '.png';
+ } elseif ( ! empty( $addon['icon'] ) && filter_var( $addon['icon'], FILTER_VALIDATE_URL ) ) {
+ $icon = esc_url( $addon['icon'] );
} else {
- $icon = charitable()->get_path( 'assets', false ) . 'images/addons/addon-icon-' . $icon_file_name . '.png';
+ $icon = $placeholder_icon;
}
// get the plugin description.
- $sections = unserialize( $addon['sections'] ); // phpcs:ignore
- $description = wp_strip_all_tags( ( $sections['description'] ) );
+ // Guard against a missing / malformed `sections` entry from the API so a
+ // single bad addon row can't blow up the whole directory render. Belt +
+ // suspenders: the conditions below handle normal failure modes, and the
+ // try/catch handles anything pathological.
+ $sections = array();
+ $description = '';
+ try {
+ if ( isset( $addon['sections'] ) && is_string( $addon['sections'] ) && '' !== $addon['sections'] ) {
+ $maybe_sections = @unserialize( $addon['sections'], array( 'allowed_classes' => false ) ); // phpcs:ignore
+ if ( is_array( $maybe_sections ) ) {
+ $sections = $maybe_sections;
+ }
+ }
+ $description = wp_strip_all_tags( isset( $sections['description'] ) ? (string) $sections['description'] : '' );
+ } catch ( Throwable $e ) {
+ $description = '';
+ if ( function_exists( 'charitable_is_debug' ) && charitable_is_debug() ) {
+ error_log( 'CHARITABLE: Addon description parse failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ }
+ }
+
+ // The wpcharitable.com addons API currently returns an empty description for
+ // the Charitable Pro entry. Provide a hard-coded fallback so the card always
+ // has a meaningful description on this screen.
+ if ( 'charitable-pro' === $addon['slug'] && '' === trim( $description ) ) {
+ $description = __( 'Charitable Pro is the upgraded version of your current Charitable plugin, with built-in features and compatibility for more advanced addons. Installing Pro will replace this Lite version while keeping all your existing settings and data.', 'charitable' );
+ }
$is_recommended = ( isset( $addon['featured'] ) && is_array( $addon['featured'] ) && in_array( 'recommended', $addon['featured'] ) ) ? true : false; // phpcs:ignore
// css.
@@ -789,8 +938,13 @@
}
// Output the card.
-
- $addon_name = str_replace( 'Charitable ', '', $addon['name'] );
+ // Most addon cards strip the redundant "Charitable " prefix so the title
+ // reads cleanly (e.g. "Fee Relief" instead of "Charitable Fee Relief").
+ // The Pro card is the exception — without the brand name it just reads as
+ // "Pro", which is meaningless. Keep the full name only for that slug.
+ $addon_name = ( 'charitable-pro' === $addon['slug'] )
+ ? $addon['name']
+ : str_replace( 'Charitable ', '', $addon['name'] );
$landing_page_url = ! empty( $addon['homepage'] ) ? $addon['homepage'] : false;
$landing_page_url = false === $landing_page_url ? $addon['upgrade_url'] : $landing_page_url;
?>
@@ -798,7 +952,7 @@
<div class="charitable-addons-list-item <?php echo esc_attr( implode( ' ', $css ) ); ?>">
<div class="charitable-addons-list-item-header">
- <img src="<?php echo esc_url( $icon ); ?>" alt="<?php echo esc_html( $addon['name'] ); ?> logo">
+ <img src="<?php echo esc_url( $icon ); ?>" alt="<?php echo esc_html( $addon['name'] ); ?> logo" onerror="this.onerror=null;this.src='<?php echo esc_url( $placeholder_icon ); ?>';">
<div class="charitable-addons-list-item-header-meta">
<div class="charitable-addons-list-item-header-meta-title">
@@ -819,7 +973,17 @@
<div class="charitable-addons-list-item-footer charitable-addons-list-item-footer-installed" data-plugin="<?php echo esc_attr( $addon['path'] ); ?>" data-type="addon">
<div>
<?php if ( ! empty( $addon['license'] ) ) : ?>
- <span class="charitable-badge charitable-badge-lg charitable-badge-inline charitable-badge-titanium charitable-badge-rounded"><?php echo esc_html( end( $addon['license'] ) ); ?></span>
+ <?php
+ // The license badge defaults to the highest tier the addon is
+ // available in. Charitable Pro is available to every legitimate
+ // license tier, so showing the "Pro" tier label is misleading —
+ // surface "All Plans" instead so customers on Basic/Plus realise
+ // they can install it too.
+ $license_badge_text = ( 'charitable-pro' === $addon['slug'] )
+ ? __( 'All Plans', 'charitable' )
+ : end( $addon['license'] );
+ ?>
+ <span class="charitable-badge charitable-badge-lg charitable-badge-inline charitable-badge-titanium charitable-badge-rounded"><?php echo esc_html( $license_badge_text ); ?></span>
<?php endif; ?>
</div>
--- a/charitable/includes/admin/campaign-builder/class-charitable-campaign-builder.php
+++ b/charitable/includes/admin/campaign-builder/class-charitable-campaign-builder.php
@@ -7,7 +7,7 @@
* @copyright Copyright (c) 2023, WP Charitable LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 1.8.0
- * @version 1.8.9.1
+ * @version 1.8.10.5
*/
// Exit if accessed directly.
@@ -227,6 +227,11 @@
$this->view = isset( $_GET['view'] ) ? esc_attr( $_GET['view'] ) : false; // phpcs:ignore
$this->campaign_id = isset( $_GET['campaign_id'] ) ? intval( $_GET['campaign_id'] ) : false; // phpcs:ignore
+ // Lite has no Form panel content; fall back if ?view=form is requested directly.
+ if ( 'form' === $this->view ) {
+ $this->view = false;
+ }
+
// if no view was past, determine if the new campaign cofmr has been used (check settings) and if not redirect to template screen.
if ( $this->campaign_id && ! $this->view ) {
$campaign_data = get_post_meta( $this->campaign_id, 'campaign_settings_v2' );
@@ -952,6 +957,17 @@
)
),
'charitable_addons_page' => esc_url( admin_url( 'admin.php?page=charitable-addons' ) ),
+ 'form_upgrade_modal' => array(
+ 'title' => esc_html__( 'Unlock the Visual Form Builder', 'charitable' ),
+ 'message' => '<p>' . esc_html__( "The Charitable Pro plugin's visual form builder lets you drag and drop fields, build multi-step donation flows, and customize every part of your donation form - all without writing code.", 'charitable' ) . '</p><p>' . esc_html__( 'Upgrade to any paid Charitable plan to unlock the Charitable Pro plugin and design your forms exactly the way you want.', 'charitable' ) . '</p>',
+ 'button' => esc_html__( 'Get Charitable Pro', 'charitable' ),
+ 'doc' => sprintf(
+ '<div class="already-purchased-div"><a href="%1$s" target="_blank" rel="noopener noreferrer" class="already-purchased">%2$s</a></div>',
+ esc_url( 'https://www.wpcharitable.com/documentation/how-to-use-donation-form-visual-builder/' ),
+ esc_html__( 'Learn More', 'charitable' )
+ ),
+ 'upgrade_url' => charitable_admin_upgrade_link( 'Campaign+Builder+Form+Panel+Modal' ),
+ ),
'charitable_license_label' => esc_html( Charitable_Licenses_Settings::get_instance()->get_license_label_from_plan_id() ),
'charitable_form_name' => $campaign_name,
'charitable_assets_dir' => apply_filters(
@@ -1156,6 +1172,7 @@
* Load panels.
*
* @since 1.0.0
+ * @version 1.8.10.5 Added Form panel (Lite upgrade-prompt stub).
*/
public function load_panels() {
@@ -1176,6 +1193,7 @@
array(
'template',
'design',
+ 'form',
'settings',
'marketing',
'payment',
--- a/charitable/includes/admin/campaign-builder/panels/class-form.php
+++ b/charitable/includes/admin/campaign-builder/panels/class-form.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Form panel (Lite upgrade-prompt stub).
+ *
+ * Lite does not ship the visual form builder. This panel renders only the
+ * navigation button; clicking it opens an upgrade modal instead of switching
+ * views. The full panel is provided by Charitable Pro.
+ *
+ * @package Charitable
+ * @author David Bisset
+ * @copyright Copyright (c) 2026, WP Charitable LLC
+ * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
+ * @since 1.8.10.5
+ * @version 1.8.10.5
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+if ( ! class_exists( 'Charitable_Builder_Panel_Form' ) ) :
+
+ /**
+ * Form panel (Lite upgrade stub).
+ *
+ * @since 1.8.10.5
+ */
+ class Charitable_Builder_Panel_Form extends Charitable_Builder_Panel {
+
+ /**
+ * All systems go.
+ *
+ * @since 1.8.10.5
+ */
+ public function init() {
+
+ $this->name = esc_html__( 'Form', 'charitable' );
+ $this->slug = 'form';
+ $this->icon = 'panel_form.svg';
+ $this->order = 25;
+ $this->sidebar = false;
+ }
+
+ /**
+ * Render the navigation button.
+ *
+ * Overrides the base implementation so the button does not carry a
+ * `data-panel` attribute — we intercept the click in JS and open an
+ * upgrade modal rather than switching panels.
+ *
+ * @since 1.8.10.5
+ *
+ * @param mixed $campaign Current campaign object.
+ * @param string $view The current view.
+ */
+ public function button( $campaign, $view ) {
+ ?>
+ <button type="button" class="charitable-panel-<?php echo esc_attr( $this->slug ); ?>-button charitable-panel-form-upgrade">
+ <img class="topbar_icon" src="<?php echo esc_url( charitable()->get_path( 'assets', false ) . 'images/icons/' . $this->icon ); ?>" />
+ <span><?php echo esc_html( $this->name ); ?></span>
+ </button>
+ <?php
+ }
+
+ /**
+ * Suppress panel output — Lite has no Form panel content.
+ *
+ * @since 1.8.10.5
+ *
+ * @param object $campaign Current campaign object.
+ * @param string $view Active Campaign Builder view (panel).
+ */
+ public function panel_output( $campaign, $view = 'design' ) {
+ }
+ }
+
+endif;
+
+new Charitable_Builder_Panel_Form();
--- a/charitable/includes/admin/charitable-core-admin-functions.php
+++ b/charitable/includes/admin/charitable-core-admin-functions.php
@@ -1208,13 +1208,23 @@
// attempt to activate the installed addon, save the user a step.
$activate = activate_plugins( $plugin_basename );
if ( ! is_wp_error( $activate ) ) {
- wp_send_json_success(
- array(
- 'basename' => $plugin_basename,
- 'is_activated' => true,
- 'msg' => esc_html__( 'Addon installed and activated.', 'charitable' ),
- )
+ $response = array(
+ 'basename' => $plugin_basename,
+ 'is_activated' => true,
+ 'msg' => esc_html__( 'Addon installed and activated.', 'charitable' ),
);
+
+ // When Charitable Pro is what was just installed and activated, point
+ // the user at the Charitable dashboard. Activating Pro auto-deactivates
+ // Lite, which means the addons screen they're currently on (served by
+ // Lite) no longer exists in the menu — without a redirect they'd land
+ // on a 404 / blank admin page once the AJAX returns. Pro's main file
+ // lives at charitable-pro/charitable.php, so we match by directory.
+ if ( 0 === strpos( $plugin_basename, 'charitable-pro/' ) ) {
+ $response['redirect'] = admin_url( 'admin.php?page=charitable-dashboard' );
+ }
+
+ wp_send_json_success( $response );
} else {
wp_send_json_success(
array(
@@ -1530,6 +1540,22 @@
// add a 'dismissed' key to the notification with the current time.
$notifications[ $notification_id ]['dismissed'] = time();
update_option( 'charitable_dashboard_notifications', $notifications );
+
+ /**
+ * Fires after a dashboard rail notification has been dismissed.
+ *
+ * Listeners can attach side effects keyed off the dismissed slug —
+ * e.g. the resume-setup-wizard module clears
+ * `charitable_started_onboarding` when its banner is dismissed,
+ * so the underlying onboarding state is also turned off, not just
+ * the banner.
+ *
+ * @since 1.8.10.5
+ *
+ * @param string $notification_id Slug of the dismissed notification.
+ */
+ do_action( 'charitable_dashboard_notification_dismissed', $notification_id );
+
wp_send_json_success( array( 'message' => esc_html__( 'Notification removed.', 'charitable' ) ) );
} else {
wp_send_json_error( array( 'message' => esc_html__( 'Notification not found.', 'charitable' ) ) );
--- a/charitable/includes/admin/class-charitable-admin.php
+++ b/charitable/includes/admin/class-charitable-admin.php
@@ -569,7 +569,7 @@
if ( ! is_null( $screen ) && ( $screen->id === 'charitable_page_charitable-reports' || $screen->id === 'charitable_page_charitable-dashboard' ) ) {
// Specific styles for the "overview" and main reporting tabs.
- if ( empty( $_GET['tab'] ) || ( ! empty( $_GET['tab'] && charitable_reports_allow_tab_load_scripts( strtolower( $_GET['tab'] ) ) ) ) ) { // phpcs:ignore
+ if ( empty( $_GET['tab'] ) || ( ! empty( $_GET['tab'] ) && charitable_reports_allow_tab_load_scripts( strtolower( sanitize_text_field( wp_unslash( $_GET['tab'] ) ) ) ) ) ) { // phpcs:ignore
wp_register_script(
'charitable-apex-charts',
@@ -619,7 +619,7 @@
wp_enqueue_script( 'charitable-report-date-range-picker' );
wp_enqueue_script( 'charitable-reporting' );
- } else if ( ! empty( $_GET['tab'] && 'analytics' === $_GET['tab'] ) ) { // phpcs:ignore
+ } elseif ( ! empty( $_GET['tab'] ) && 'analytics' === $_GET['tab'] ) { // phpcs:ignore
// this loads a specific script for the analytics tab.
do_action( 'charitable_admin_enqueue_analytics_scripts' );
@@ -1258,7 +1258,7 @@
*/
public function export_donations() {
- if ( ! wp_verify_nonce( $_GET['_charitable_export_nonce'], 'charitable_export_donations' ) ) { // phpcs:ignore
+ if ( ! isset( $_GET['_charitable_export_nonce'] ) || ! wp_verify_nonce( $_GET['_charitable_export_nonce'], 'charitable_export_donations' ) ) { // phpcs:ignore
return false;
}
@@ -1581,6 +1581,20 @@
wp_send_json_success( array( 'form' => $form ) );
}
+ // Snapshot installed plugins BEFORE the install so we can identify what
+ // was actually unzipped. The legacy slug-derivation in
+ // charitable_get_plugin_basename_from_slug() reduces a download URL like
+ // `…/charitable-pro-plugin-1.8.13.5.zip` to the slug
+ // `charitable-pro-plugin-1.8.13.5`, then expects an installed plugin
+ // directory whose name *starts* with that slug — which doesn't hold for
+ // any addon whose zip filename includes a version suffix and whose
+ // directory does not (e.g. Charitable Pro lives at `charitable-pro/`).
+ // The before/after diff sidesteps that mismatch entirely.
+ if ( ! function_exists( 'get_plugins' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+ $plugins_before = array_keys( get_plugins() );
+
// Download and install the plugin.
$result = charitable_install_wporg_plugin( $plugin );
@@ -1588,11 +1602,21 @@
wp_send_json_error( $result->get_error_message() );
}
- // Get plugin basename.
- $plugin_basename = charitable_get_plugin_basename_from_slug( $plugin );
+ // Force a fresh re-scan of the plugins directory and compute the diff.
+ wp_cache_delete( 'plugins', 'plugins' );
+ $plugins_after = array_keys( get_plugins() );
+ $newly_installed = array_values( array_diff( $plugins_after, $plugins_before ) );
+
+ if ( ! empty( $newly_installed ) ) {
+ $plugin_basename = $newly_installed[0];
+ } else {
+ // Fallback to the legacy slug-derived guess if for some reason no new
+ // plugin was detected (e.g. the zip overwrote an existing install).
+ $plugin_basename = charitable_get_plugin_basename_from_slug( $plugin );
+ }
wp_send_json_success( array(
- 'msg' => 'Plugin installed successfully',
+ 'msg' => __( 'Plugin installed successfully', 'charitable' ),
'basename' => $plugin_basename,
'is_activated' => false,
) );
@@ -1635,7 +1659,17 @@
wp_send_json_error( $result->get_error_message() );
}
- wp_send_json_success( 'Plugin activated successfully' );
+ $response = array( 'msg' => __( 'Plugin activated successfully', 'charitable' ) );
+
+ // Activating Charitable Pro auto-deactivates Charitable Lite, which means
+ // the screen the user came from (page=charitable-addons, served by Lite)
+ // no longer exists in the menu. Redirect them to the Charitable dashboard
+ // so they don't land on a 404 / blank admin page after the AJAX returns.
+ if ( 0 === strpos( $plugin, 'charitable-pro/' ) ) {
+ $response['redirect'] = admin_url( 'admin.php?page=charitable-dashboard' );
+ }
+
+ wp_send_json_success( $response );
}
/**
--- a/charitable/includes/admin/dashboard/class-charitable-dashboard-ajax.php
+++ b/charitable/includes/admin/dashboard/class-charitable-dashboard-ajax.php
@@ -97,9 +97,10 @@
$clear_cache = ( isset( $_GET['charitable_clear_stats_cache'] ) && '1' === $_GET['charitable_clear_stats_cache'] ) ||
( isset( $_POST['charitable_clear_stats_cache'] ) && '1' === $_POST['charitable_clear_stats_cache'] );
+ $response_data = array();
+
if ( $clear_cache ) { // phpcs:ignore
$charitable_dashboard->clear_dashboard_stats_cache();
- // Add a flag to indicate cache was cleared
$response_data['cache_cleared'] = true;
}
@@ -135,10 +136,10 @@
);
// Prepare response data.
- $response_data = array(
+ $response_data = array_merge( $response_data, array(
'stats' => $stats,
'chart' => $chart_data,
- );
+ ) );
wp_send_json_success( $response_data );
--- a/charitable/includes/admin/dashboard/class-charitable-dashboard.php
+++ b/charitable/includes/admin/dashboard/class-charitable-dashboard.php
@@ -77,6 +77,52 @@
// Hook into Charitable's cache clearing mechanism
add_action( 'charitable_after_clear_expired_options', array( $this, 'clear_dashboard_stats_cache' ) );
+
+ // Refresh the "resume setup wizard" notification entry just before
+ // the dashboard rail renders.
+ add_action( 'charitable_before_admin_dashboard_v2', array( $this, 'sync_resume_wizard_notification' ) );
+ }
+
+ /**
+ * Register or refresh the "resume setup wizard" entry in the dashboard
+ * notification rail when an onboarding session is in progress; remove
+ * any stale entry when it isn't.
+ *
+ * Fires on `charitable_before_admin_dashboard_v2`, immediately before
+ * `render_dashboard_notifications()` reads the same option.
+ *
+ * @since 1.8.10.5
+ *
+ * @return void
+ */
+ public function sync_resume_wizard_notification() {
+
+ $notifications = (array) get_option( 'charitable_dashboard_notifications', array() );
+ $started = 1 === (int) get_option( 'charitable_started_onboarding', 0 );
+ $present = isset( $notifications['resume_setup_wizard'] );
+ $dismissed = $present && isset( $notifications['resume_setup_wizard']['dismissed'] );
+
+ if ( $started && ! $dismissed ) {
+
+ $notifications['resume_setup_wizard'] = array(
+ 'type' => 'notice',
+ 'priority' => 3,
+ 'title' => __( 'Finish Setting Up Charitable', 'charitable' ),
+ 'message' => '<p>' . esc_html__( "Looks like you started the setup wizard but didn't finish. Pick up where you left off and we'll have you ready to accept donations in just a couple more steps.", 'charitable' ) . '</p>',
+ 'custom_css' => 'charitable-notification-type-notice',
+ 'button_url' => add_query_arg( array( 'resume' => 'true' ), charitable_get_onboarding_url() ),
+ 'button_text' => __( 'Resume Setup Wizard', 'charitable' ),
+ );
+
+ update_option( 'charitable_dashboard_notifications', $notifications );
+
+ return;
+ }
+
+ if ( $present && ! $started ) {
+ unset( $notifications['resume_setup_wizard'] );
+ update_option( 'charitable_dashboard_notifications', $notifications );
+ }
}
--- a/charitable/includes/admin/onboarding/class-charitable-setup.php
+++ b/charitable/includes/admin/onboarding/class-charitable-setup.php
@@ -136,6 +136,10 @@
*/
public function hooks() {
+ // Register the resume-banner dismiss listener unconditionally — it
+ // must fire during admin-ajax, which the guards below bail out of.
+ add_action( 'charitable_dashboard_notification_dismissed', [ $this, 'maybe_clear_onboarding_state_on_dismiss' ] );
+
// If user is in admin ajax or doing cron, return.
if ( wp_doing_ajax() || wp_doing_cron() ) {
return;
@@ -208,15 +212,21 @@
return;
}
- // Don't redirect if user is on a non-Charitable admin page.
- if ( ! empty( $_GET['page'] ) && strpos( $_GET['page'], 'charitable' ) === false ) {
+ // Don't redirect when the user is on any Charitable admin page
+ // OTHER than the welcome destination (`?page=charitable`).
+ // Charitable's sub-pages — `charitable-dashboard`, `charitable-settings-checklist`,
+ // etc. — each have their own menu_slug, so strict equality keeps the
+ // redirect scoped to `?page=charitable` itself (which IS the welcome
+ // screen) rather than yanking the user back from every sub-page.
+ if ( ! empty( $_GET['page'] ) && is_string( $_GET['page'] ) && 'charitable' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
- // Don't redirect from specific WordPress admin pages without page parameter.
- $current_page = basename( $_SERVER['PHP_SELF'] ?? '' );
- $wp_admin_pages = array( 'edit.php', 'post-new.php', 'upload.php', 'users.php', 'edit-tags.php', 'edit-comments.php', 'themes.php', 'plugins.php', 'tools.php', 'options-general.php' );
- if ( in_array( $current_page, $wp_admin_pages ) ) {
+ // Don't redirect from standard WordPress admin scripts that don't
+ // use a `?page=` parameter (e.g. the WP Dashboard, CPT lists, media library).
+ $current_page = basename( $_SERVER['PHP_SELF'] ?? '' );
+ $wp_admin_pages = array( 'index.php', 'edit.php', 'post-new.php', 'upload.php', 'users.php', 'edit-tags.php', 'edit-comments.php', 'themes.php', 'plugins.php', 'tools.php', 'options-general.php' );
+ if ( in_array( $current_page, $wp_admin_pages, true ) ) {
return;
}
@@ -236,13 +246,41 @@
}
// Build the URL for going back to the onboarding process.
- $onboarding_url = 'https://app.wpcharitable.com/setup-wizard-charitable_lite&resume=' . charitable_get_site_token();
+ $onboarding_url = add_query_arg( array( 'resume' => 'true' ), charitable_get_onboarding_url() );
wp_safe_redirect( admin_url( 'admin.php?page=charitable&wpchar_lite=lite&setup=welcome&resume=true' ) );
exit;
}
/**
+ * Clear the in-progress onboarding flag when the user dismisses the
+ * dashboard "resume setup wizard" banner.
+ *
+ * Triggered by the existing notification rail's dismiss AJAX. The
+ * action fires for ALL dismissed notifications, so we filter on slug
+ * here.
+ *
+ * Net effect: dismissing the banner ends the resume prompt everywhere.
+ * The banner is gone (the rail's existing dismissed-key filter handles
+ * that), and the `?page=charitable` resume redirect also defuses
+ * because it keys off the same option we delete here.
+ *
+ * @since 1.8.10.5
+ *
+ * @param string $notification_id Slug of the dismissed notification.
+ *
+ * @return void
+ */
+ public function maybe_clear_onboarding_state_on_dismiss( $notification_id ) {
+
+ if ( 'resume_setup_wizard' !== $notification_id ) {
+ return;
+ }
+
+ delete_option( 'charitable_started_onboarding' );
+ }
+
+ /**
* Onboarding welcome screen redirect.
*
* This function checks if a new install or update has just occurred. If so,
--- a/charitable/includes/admin/reports/class-charitable-reports.php
+++ b/charitable/includes/admin/reports/class-charitable-reports.php
@@ -375,7 +375,7 @@
<p class="cr"><?php echo esc_html__( 'Goal', 'charitable' ); ?>: <strong><?php echo charitable_format_money( $donation_goal ); // phpcs:ignore ?></strong></p>
<?php endif; ?>
<?php if ( $end_date ) : ?>
- <p class="cr"><?php echo esc_html__( 'End Date', 'charitable' ); ?>: <strong><?php echo $end_date; // phpcs:ignore ?></strong></p>
+ <p class="cr"><?php echo esc_html__( 'End Date', 'charitable' ); ?>: <strong><?php echo esc_html( $end_date ); ?></strong></p>
<?php endif; ?>
</div>
</div>
@@ -761,13 +761,13 @@
case 'charitable-failed':
$admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#';
$admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false;
- $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . $activity->campaign_title . '</span></a> ' : '';
+ $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . esc_html( $activity->campaign_title ) . '</span></a> ' : '';
$secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, 2, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '</p>';
break;
default:
$admin_campaign_url = ! empty( $activity->campaign_id ) ? charitable_get_admin_campaign_edit_url( $activity->campaign_id ) : '#';
$admin_donation_url = ! empty( $activity->item_id ) ? charitable_get_admin_donation_edit_url( $activity->item_id ) : false;
- $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . $activity->campaign_title . '</span></a> ' : '';
+ $campaign_title = ! empty( $activity->campaign_title ) ? ' to <a target="_blank" href="' . $admin_campaign_url . '"><span class="campaign-title">' . esc_html( $activity->campaign_title ) . '</span></a> ' : '';
$secondary_info = $admin_donation_url ? '<p class="charitable-reports-activity-secondary-info amount"><a href="' . $admin_donation_url . '" target="_blank">' . charitable_format_money( $activity->amount, 2, true ) . '</a>' . $campaign_title . '</p>' : '<p class="charitable-reports-activity-secondary-info amount">' . charitable_format_money( $activity->amount, 2, true ) . $campaign_title . '</p>';
break;
}
@@ -790,10 +790,10 @@
switch ( $activity->primary_action ) {
case 'update':
- $secondary_info = '<p class="campaign-title">' . $activity->campaign_title . '</p>';
+ $secondary_info = '<p class="campaign-title">' . esc_html( $activity->campaign_title ) . '</p>';
break;
default:
- $secondary_info = '<p class="campaign-title">' . $activity->campaign_title . '</p>';
+ $secondary_info = '<p class="campaign-title">' . esc_html( $activity->campaign_title ) . '</p>';
break;
}
--- a/charitable/includes/admin/settings/class-charitable-advanced-settings.php
+++ b/charitable/includes/admin/settings/class-charitable-advanced-settings.php
@@ -238,6 +238,9 @@
$empty_transient = new stdClass();
set_site_transient( 'update_plugins', $empty_transient ); // Depreciated item.
+ // Clear Stripe webhook failure counter so stale notices don't persist after fixing the root cause.
+ delete_transient( 'charitable_stripe_webhook_verification_failures' );
+
// Allow an addon to hook into this.
do_action( 'charitable_after_clear_expired_options' );
--- a/charitable/includes/admin/splash/class-charitable-admin-splash.php
+++ b/charitable/includes/admin/splash/class-charitable-admin-splash.php
@@ -234,134 +234,187 @@
public function retrieve_sections_for_user( array $sections = array() ): array {
$sections = array(
+ // Hero: Mini Donation Widget upsell with Vimeo video.
+ array(
+ 'new-for-pro' => true,
+ 'layout' => 'fifty-fifty',
+ 'class' => 'no-order',
+ 'title' => __( 'Mini Donation Widget', 'charitable' ),
+ 'content' => __( 'Embed a compact donation form anywhere on your site, no page redirect required. Add it to sidebars, landing pages, or any widget area using a simple block or shortcode.', 'charitable' ),
+ 'video' => array(
+ 'vimeo_id' => '1186687749',
+ ),
+ 'buttons' => array(
+ 'main' => array(
+ 'text' => __( 'Get Started', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/documentation/mini-donation-widget/', 'splash-modal', 'Mini Donation Widget Main' ),
+ ),
+ 'upgrade' => array(
+ 'text' => __( 'Upgrade to Pro', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/lite-upgrade/', 'splash-modal', 'Mini Donation Widget Upgrade' ),
+ ),
+ ),
+ ),
+ // 1.8.10 lite headline feature.
array(
'new' => true,
- 'version' => '1.8.9',
- 'layout' => 'fifty-fifty',
+ 'version' => '1.8.10',
+ 'layout' => 'one-third-two-thirds',
'class' => 'no-order',
- 'title' => __( 'Security Enhancements', 'charitable' ),
- 'content' => __( 'Charitable Lite now supports Google reCAPTCHA, hCaptcha, and Cloudflare Turnstile for improved security.', 'charitable' ),
+ 'title' => __( 'Migration & Import Tools', 'charitable' ),
+ 'content' => __( 'Move from GiveWP or GiveButter to Charitable in minutes. Expanded import tools include CSV donations import and a new GiveWP Migration Tool (Beta) under a redesigned, tabbed interface.', 'charitable' ),
'img' => array(
- 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-9-security.png',
+ 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-10-import-tools.svg',
'shadow' => 'none',
),
'buttons' => array(
'main' => array(
'text' => __( 'Get Started', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/get-started/1-8-9-security/', 'splash-modal', 'Square Widgets Main' ),
- ),
- 'secondary' => array(
- 'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/learn-more/1-8-9-security/', 'splash-modal', 'Square Widgets Secondary' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/how-to-switch-from-givewp-to-charitable/', 'splash-modal', 'Import Tools Main' ),
),
),
),
+ // 1.8.9 lite security feature (kept).
array(
'new' => true,
- 'version' => '1.8.8',
- 'layout' => 'fifty-fifty',
+ 'version' => '1.8.9',
+ 'layout' => 'one-third-two-thirds',
'class' => 'no-order',
- 'title' => __( 'New Dashboard!', 'charitable' ),
- 'content' => __( 'Charitable now has a new dashboard design with top campaigns, latest donations, top donors, and comments. New "30 Day" period added.', 'charitable' ),
+ 'title' => __( 'Security Enhancements', 'charitable' ),
+ 'content' => __( 'Charitable Lite now supports Google reCAPTCHA, hCaptcha, and Cloudflare Turnstile for improved security.', 'charitable' ),
'img' => array(
- 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-8-dashboard.png',
+ 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-9-security.svg',
'shadow' => 'none',
),
'buttons' => array(
'main' => array(
'text' => __( 'Get Started', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/get-started/square/', 'splash-modal', 'Square Widgets Main' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/introducing-improved-security-and-clean-donation-tool/', 'splash-modal', 'Security Main' ),
+ ),
+ ),
+ ),
+ // Pro upsells.
+ array(
+ 'new-for-pro' => true,
+ 'layout' => 'one-third-two-thirds',
+ 'class' => 'no-order',
+ 'title' => __( 'Campaign Showcase', 'charitable' ),
+ 'content' => __( 'Display all your campaigns beautifully with full layout control including grid, list, or masonry. No coding required.', 'charitable' ),
+ 'img' => array(
+ 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-13-campaign-showcase.png',
+ 'shadow' => 'none',
+ ),
+ 'buttons' => array(
+ 'main' => array(
+ 'text' => __( 'Get Started', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/campaign-showcase-getting-started/', 'splash-modal', 'Campaign Showcase Main' ),
),
- 'secondary' => array(
- 'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/learn-more/square/', 'splash-modal', 'Square Widgets Secondary' ),
+ 'upgrade' => array(
+ 'text' => __( 'Upgrade to Pro', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/lite-upgrade/', 'splash-modal', 'Campaign Showcase Upgrade' ),
),
),
),
array(
'new-for-pro' => true,
- 'layout' => 'one-third-two-thirds-flipped',
- 'class' => 'no-order',
- 'title' => __( 'Advanced Elementor Widgets', 'charitable' ),
- 'content' => __( 'When you create pages with the Elementor page builder, you'll now find four ready-made Charitable widgets (campaigns, donation button, donation form, campaigns).', 'charitable' ),
- 'img' => array(
- 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-8-elementor.png',
+ 'layout' => 'one-third-two-thirds',
+ 'class' => 'no-order',
+ 'title' => __( 'Donations Feed', 'charitable' ),
+ 'content' => __( 'Display recent donations in beautiful list or card views with sorting, filtering, pagination, and live polling that automatically refreshes when new donations arrive.', 'charitable' ),
+ 'img' => array(
+ 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-13-donate-feed.png',
'shadow' => 'none',
),
- 'buttons' => array(
+ 'buttons' => array(
'main' => array(
'text' => __( 'Get Started', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/documentation/how-to-use-charitable-widgets-in-elementor/', 'splash-modal', 'Elementor Widgets Main' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/donations-feed-getting-started/', 'splash-modal', 'Donations Feed Main' ),
),
- 'secondary' => array(
- 'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/introducing-charitable-1-8-6-elementor-widgets-reply-to-and-new-splash-screen/', 'splash-modal', 'Elementor Widgets Secondary' ),
+ 'upgrade' => array(
+ 'text' => __( 'Upgrade to Pro', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/lite-upgrade/', 'splash-modal', 'Donations Feed Upgrade' ),
),
),
),
array(
- 'new-addon' => true,
- 'layout' => 'one-third-two-thirds',
- 'class' => 'no-order',
- 'title' => __( 'DonorTrust', 'charitable' ),
- 'content' => __( 'Showcase real-time, verified donations to your website visitors and encourage more people to donate to your cause.', 'charitable' ),
- 'img' => array(
- 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-8-donortrust.gif',
+ 'new-for-pro' => true,
+ 'layout' => 'one-third-two-thirds',
+ 'class' => 'no-order',
+ 'title' => __( 'Campaign Modal Button', 'charitable' ),
+ 'content' => __( 'Add a donate button anywhere on your site that opens a donation form in a modal popup, no page redirect required. Available as a block or shortcode.', 'charitable' ),
+ 'img' => array(
+ 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-13-modal-button.png',
'shadow' => 'none',
),
- 'buttons' => array(
+ 'buttons' => array(
'main' => array(
'text' => __( 'Get Started', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/documentation/charitable-donortrust/', 'splash-modal', 'DonorTrust Main' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/campaign-modal-button-getting-started/', 'splash-modal', 'Campaign Modal Button Main' ),
),
- 'secondary' => array(
- 'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/introducing-donortrust/', 'splash-modal', 'DonorTrust Secondary' ),
+ 'upgrade' => array(
+ 'text' => __( 'Upgrade to Pro', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/lite-upgrade/', 'splash-modal', 'Campaign Modal Button Upgrade' ),
),
),
),
array(
- 'new-for-pro' => true,
- 'layout' => 'one-third-two-thirds-flipped',
- 'class' => 'no-order',
- 'title' => __( 'More Stripe Options!', 'charitable' ),
- 'content' => __( 'Charitable now supports ACH Direct Debit, SEPA Direct Debit, Cash App, and BECS Direct Debit for Stripe users.', 'charitable' ),
- 'img' => array(
- 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-8-stripe.png',
+ 'new-for-pro' => true,
+ 'layout' => 'one-third-two-thirds',
+ 'class' => 'no-order',
+ 'title' => __( 'Prefill Donation Forms', 'charitable' ),
+ 'content' => __( 'Pre-populate donation form fields via URL query strings. Perfect for email campaigns, targeted landing pages, and personalized donor outreach.', 'charitable' ),
+ 'img' => array(
+ 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-13-prefill-forms.png',
'shadow' => 'none',
),
- 'buttons' => array(
+ 'buttons' => array(
'main' => array(
'text' => __( 'Get Started', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/introducing-charitable-1-8-8/', 'splash-modal', 'Square Widgets Main' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/prefill-donation-forms-getting-started/', 'splash-modal', 'Prefill Donation Forms Main' ),
),
- 'secondary' => array(
- 'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/introducing-charitable-1-8-8/', 'splash-modal', 'Square Widgets Secondary' ),
+ 'upgrade' => array(
+ 'text' => __( 'Upgrade to Pro', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/lite-upgrade/', 'splash-modal', 'Prefill Donation Forms Upgrade' ),
),
),
),
array(
- 'new-addon' => true,
- 'layout' => 'one-third-two-thirds',
- 'class' => 'no-order',
- 'title' => __( 'Google Analytics', 'charitable' ),
- 'content' => __( 'The new Google Analytics addon means you can track your campaign performance and see how your donors are engaging with your campaign.', 'charitable' ),
- 'img' => array(
- 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-7-ga.png',
+ 'new-for-pro' => true,
+ 'layout' => 'one-third-two-thirds',
+ 'class' => 'no-order',
+ 'title' => __( 'Campaign Featured Image', 'charitable' ),
+ 'content' => __( 'Set your campaign's featured image directly from the Campaign Builder. No need to switch to the post editor. Perfect for giving your campaigns a polished, visual identity.', 'charitable' ),
+ 'img' => array(
+ 'url' => charitable()->get_path( 'assets', false ) . 'images/splash/1-8-13-featured-image.png',
'shadow' => 'none',
),
- 'buttons' => array(
+ 'buttons' => array(
'main' => array(
'text' => __( 'Get Started', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/get-started/google-analytics/', 'splash-modal', 'GA Main' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/campaign-featured-image-getting-started/', 'splash-modal', 'Campaign Featured Image Main' ),
),
- 'secondary' => array(
- 'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/learn-more/google-analytics/', 'splash-modal', 'GA Secondary' ),
+ 'upgrade' => array(
+ 'text' => __( 'Upgrade to Pro', 'charitable' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/lite-upgrade/', 'splash-modal', 'Campaign Featured Image Upgrade' ),
),
),
),
+ // Two-column "More Recent Features" list.
+ array(
+ 'layout' => 'more-features',
+ 'class' => 'no-order',
+ 'title' => __( 'More Recent Features:', 'charitable' ),
+ 'items' => array(
+ __( 'Campaign Selector', 'charitable' ),
+ __( 'Envira Gallery Integration', 'charitable' ),
+ __( 'Visual Form Builder', 'charitable' ),
+ __( 'Donor Leaderboards', 'charitable' ),
+ __( 'Magic Donor Dashboard Link', 'charitable' ),
+ __( 'DIVI Integration', 'charitable' ),
+ __( 'DonorTrust', 'charitable' ),
+ __( 'Google Analytics', 'charitable' ),
+ ),
+ ),
);
return $sections;
@@ -525,20 +578,20 @@
// If the chartiable_pro is active, that means they are licensed but not using Charitable Pro plugin.
if ( ! charitable_is_pro() ) :
$default_data['footer'] = array(
- 'title' => __( 'Add Your License To Activate Charitable Pro Plugin Now!', 'charitable' ),
+ 'title' => __( 'Add Your License To Activate Charitable Pro Plugin Now And Start Getting More Donations!', 'charitable' ),
'description' => __( 'Charitable Pro is a powerful upgrade that allows you to manage donors along with built-in features like videos, donor comments, PDF receipts, a dashboard for donors, and more.', 'charitable' ),
'upgrade' => array(
'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/introducing-charitable-pro/', 'splash-modal', 'learn-more' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/lite-upgrade/', 'splash-modal', 'learn-more' ),
),
);
else :
$default_data['footer'] = array(
- 'title' => __( 'Thank you for using Charitable Pro!', 'charitable' ),
- 'description' => __( 'We hope you love the new features and updates we've made to Charitable Pro. Learn more about the latest updates and improvements.', 'charitable' ),
+ 'title' => __( 'Upgrade To The Charitable Pro Plugin At No Cost!', 'charitable' ),
+ 'description' => __( 'Registered users with active license can upgrade to Charitable Pro plugin at NO COST. It's included in all plans (basic, plus, pro, elite).', 'charitable' ),
'upgrade' => array(
'text' => __( 'Learn More', 'charitable' ),
- 'url' => charitable_utm_link( 'https://www.wpcharitable.com/blog/', 'splash-modal', 'learn-more' ),
+ 'url' => charitable_utm_link( 'https://www.wpcharitable.com/pricing/upgrade-lite-to-pro/', 'splash-modal', 'learn-more' ),
),
);
endif;
--- a/charitable/includes/admin/templates/splash/splash-section.php
+++ b/charitable/includes/admin/templates/splash/splash-section.php
@@ -3,11 +3,13 @@
* What's New modal section.
*
* @since 1.8.8
- * @version 1.8.9.1
+ * @version 1.8.10.6
*
* @var string $title Section title.
* @var string $content Section content.
* @var array $img Section image.
+ * @var array $video Section video (Vimeo or mp4).
+ * @var array $items More-features layout items.
* @var string $new Is new feature.
* @var array $buttons Section buttons.
* @var string $layout Section layout.
@@ -29,6 +31,18 @@
// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
?>
+<?php if ( 'more-features' === $section['layout'] ) : ?>
+<section class="charitable-splash-section charitable-splash-section-more-features">
+ <h3><?php echo esc_html( $section['title'] ); ?></h3>
+ <?php if ( ! empty( $section['items'] ) ) : ?>
+ <ul class="charitable-splash-more-features-list">
+ <?php foreach ( $section['items'] as $item ) : ?>
+ <li><?php echo esc_html( $item ); ?></li>
+ <?php endforeach; ?>
+ </ul>
+ <?php endif; ?>
+</section>
+<?php else : ?>
<section class="<?php echo esc_attr( $classes_output ); ?>">
<div class="charitable-splash-section-content">
<?php
@@ -73,7 +87,13 @@
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
// Local template variables scoped to this foreach loop.
foreach ( $section['buttons'] as $button_type => $button ) {
- $button_class = $button_type === 'main' ? 'charitable-btn-orange' : 'charitable-btn-bordered';
+ if ( 'main' === $button_type ) {
+ $button_class = 'charitable-btn-orange';
+ } elseif ( 'upgrade' === $button_type ) {
+ $button_class = 'charitable-btn-green';
+ } else {
+ $button_class = 'charitable-btn-bordered';
+ }
printf(
'<a href="%1$s" class="charitable-btn %3$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
@@ -88,7 +108,23 @@
<?php endif; ?>
</div>
- <?php if ( ! empty( $section['img'] ) ) : ?>
+ <?php if ( ! empty( $section['video'] ) ) : ?>
+ <div class="charitable-splash-section-image charitable-splash-video-wrap">
+ <?php if ( ! empty( $section['video']['vimeo_id'] ) ) : ?>
+ <?php
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Local template variable.
+ $vimeo_id = preg_replace( '/[^0-9]/', '', (string) $section['video']['vimeo_id'] );
+ ?>
+ <div class="charitable-splash-video-embed"
+ data-vimeo-id="<?php echo esc_attr( $vimeo_id ); ?>"
+ data-video-title="<?php echo esc_attr( $section['title'] ); ?>"></div>
+ <?php elseif ( ! empty( $section['video']['url'] ) ) : ?>
+ <video autoplay muted playsinline controls>
+ <source src="<?php echo esc_url( $section['video']['url'] ); ?>" type="video/mp4">
+ </video>
+ <?php endif; ?>
+ </div>
+ <?php elseif ( ! empty( $section['img'] ) ) : ?>
<?php
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Local template variable scoped to this conditional block.
$shadow_class = charitable_sanitize_classes( $section['img']['shadow'] ?? 'none' );
@@ -98,3 +134,4 @@
</div>
<?php endif; ?>
</section>
+<?php endif; ?>
--- a/charitable/includes/admin/tools/charitable-tools-admin-hooks.php
+++ b/charitable/includes/admin/tools/charitable-tools-admin-hooks.php
@@ -146,6 +146,15 @@
*/
add_action( 'wp_ajax_charitable_debug_log_scan', array( Charitable_Tools_System_Info::get_instance(), 'ajax_debug_log_scan' ) );
+/**
+ * Capture failed plugin installs/updates into a small ring buffer so the
+ * "Recent Plugin Install/Update Errors" section of System Info has data
+ * to show. Pure pass-through on success.
+ *
+ * @since 1.8.10.5
+ */
+add_filter( 'upgrader_install_package_result', array( 'Charitable_Tools_System_Info', 'log_install_result' ), 10, 2 );
+
add_action( 'admin_enqueue_scripts', array( Charitable_Tools_Misc::get_instance(), 'enqueue_scripts' ) );
/**
--- a/charitable/includes/admin/tools/class-charitable-tools-system-info.php
+++ b/charitable/includes/admin/tools/class-charitable-tools-system-info.php
@@ -355,6 +355,7 @@
$data .= $this->email_diagnostics();
$data .= $this->donation_error_logs();
$data .= $this->debug_log_scanner();
+ $data .= $this->hosting_environment_info();
$data .= "n" . '### End System Info ###';
@@ -1683,6 +1684,298 @@
return self::$instance;
}
+
+ /**
+ * Hosting Environment, Filesystem, and recent Plugin Install error details.
+ *
+ * Designed to surface the signals that matter most when troubleshooting
+ * plugin upload/install failures on managed hosts (WordPress.com, WP
+ * Engine, Kinsta, etc.) where standard PHP limits are misleading.
+ *
+ * @since 1.8.10.5
+ *
+ * @return string
+ */
+ private function hosting_environment_info() {
+
+ $data = "n" . '-- Hosting Environment' . "nn";
+ $data .= 'Detected Host: ' . self::detect_host() . "n";
+ $data .= 'Reverse Proxy/CDN: ' . self::detect_proxy() . "n";
+
+ $data .= "n" . '-- Filesystem & Updates' . "nn";
+ $data .= 'DISALLOW_FILE_EDIT: ' . self::format_constant_state( 'DISALLOW_FILE_EDIT' ) . "n";
+ $data .= 'DISALLOW_FILE_MODS: ' . self::format_constant_state( 'DISALLOW_FILE_MODS' ) . "n";
+ $data .= 'AUTOMATIC_UPDATER_DISABLED: ' . self::format_constant_state( 'AUTOMATIC_UPDATER_DISABLED' ) . "n";
+ $data .= 'WP_AUTO_UPDATE_CORE: ' . self::format_constant_state( 'WP_AUTO_UPDATE_CORE' ) . "n";
+ $data .= 'FS_METHOD: ' . ( defined( 'FS_METHOD' ) ? esc_html( (string) FS_METHOD ) : '(auto-detect)' ) . "n";
+ $data .= 'Plugin Dir Writable: ' . ( is_writable( WP_PLUGIN_DIR ) ? 'Yes' : 'No' ) . ' (' . WP_PLUGIN_DIR . ')' . "n";
+ $data .= 'wp-content Writable: ' . ( is_writable( WP_CON