array(
'name' => 'WP Mail SMTP',
'basename' => 'wp-mail-smtp/wp_mail_smtp.php',
),
'optinmonster' => array(
'name' => 'OptinMonster',
'basename' => 'optinmonster/optin-monster-wp-api.php',
),
'wpforms' => array(
'name' => 'WPForms',
'basename' => 'wpforms/wpforms.php',
),
'google-analytics-for-wordpress' => array(
'name' => 'MonsterInsights',
'basename' => 'google-analytics-for-wordpress/googleanalytics.php',
),
'all-in-one-seo-pack' => array(
'name' => 'All in One SEO',
'basename' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
),
'duplicator' => array(
'name' => 'Duplicator',
'basename' => 'duplicator/duplicator.php',
),
);
/**
* The feature meta.
*
* @since 1.8.4
*
* @var array
*/
private $feature_meta = array(
'ambassadors' => array(
'name' => 'Ambassadors',
),
'recurring' => array(
'name' => 'Recurring Donations',
),
'fee-relief' => array(
'name' => 'Fee Relief',
),
);
/**
* The private $stripe_redirect_url property.
*
* @var string
*/
private $stripe_redirect_url = '';
/**
* The single instance of this class.
*
* @var Charitable_Setup|null
*/
private static $instance = null;
/**
* Create object instance.
*
* @since 1.8.4
*/
public function __construct() {
add_action( 'plugins_loaded', [ $this, 'hooks' ] );
$this->stripe_redirect_url = admin_url( 'admin.php?page=charitable-setup&setup=stripe-connect' );
}
/**
* Run things upon init.
*
* @since 1.8.4
*
* @return void
*/
public function init() {
}
/**
* The hooks.
*
* @since 1.8.4
*/
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' ] );
// Records that the user actually departed for the setup wizard. Like the
// hook above, this MUST be registered before the wp_doing_ajax() bail
// below, or it would never fire on admin-ajax.php.
add_action( 'wp_ajax_charitable_onboarding_started', [ $this, 'ajax_mark_onboarding_started' ] );
// If user is in admin ajax or doing cron, return.
if ( wp_doing_ajax() || wp_doing_cron() ) {
return;
}
// If user cannot manage_options, return.
if ( ! charitable_current_user_can( 'administrator' ) ) {
return;
}
// Third party plugins.
add_filter( 'duplicator_disable_onboarding_redirect', '__return_true' );
// Resume onboarding.
add_action( 'init', [ $this, 'resume_onboarding' ] );
add_action( 'init', [ $this, 'cancel_onboarding' ] );
// Init.
add_action( 'admin_init', [ $this, 'redirect' ], 9999 );
}
/**
* Cancels the onboarding process.
*
* @since 1.8.4
*
* @return void
*/
public function cancel_onboarding() {
if ( ! isset( $_GET['charitable_onboarding'] ) || 'cancel' !== $_GET['charitable_onboarding'] ) { // phpcs:ignore
return;
}
// Clear the transient.
delete_transient( 'charitable_activation_redirect' );
// Clear the option.
delete_option( 'charitable_started_onboarding' );
// If the user was going to the checklist, send them there.
$checklist_class = Charitable_Checklist::get_instance();
$redirect_url = ! $checklist_class->maybe_load_checklist_assets() ? admin_url( 'admin.php?page=charitable-dashboard' ) : admin_url( 'admin.php?page=charitable-setup-checklist' );
wp_safe_redirect( $redirect_url );
exit;
}
/**
* If the option 'charitable_start_onboarding' is set, then we will start the onboarding process, assuming the user lost connection, WP site logged out by the time app.wpcharitable.com came back, etc.
*
* @since 1.8.4
* @since 1.8.9.2 Added WordPress context awareness and smart page detection.
*/
public function resume_onboarding() {
// Don't redirect during WordPress maintenance operations.
if ( defined( 'DOING_AUTOSAVE' ) || wp_is_json_request() ||
defined( 'IFRAME_REQUEST' ) || ( function_exists( 'wp_doing_heartbeat' ) && wp_doing_heartbeat() ) ) {
return;
}
// Check if we should consider redirection.
if ( false === (bool) get_option( 'charitable_started_onboarding', false ) ) {
return;
}
// Allow developers to disable resume onboarding via filter.
if ( apply_filters( 'charitable_disable_activation_redirect', false ) ) {
return;
}
// 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 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;
}
// Don't redirect if user is accessing post type or taxonomy pages.
if ( isset( $_GET['post_type'] ) || isset( $_GET['taxonomy'] ) ) {
return;
}
// Make sure we aren't already on the setup welcome page.
if ( isset( $_GET['wpchar_lite'] ) && 'lite' === $_GET['wpchar_lite'] ) { // phpcs:ignore
return;
}
// Don't redirect if user is trying to cancel onboarding - let cancel_onboarding() method handle it.
if ( isset( $_GET['charitable_onboarding'] ) && 'cancel' === $_GET['charitable_onboarding'] ) { // phpcs:ignore
return;
}
// Build the URL for going back to the onboarding process.
$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' );
}
/**
* Record that the user has actually departed for the setup wizard.
*
* Fired by a beacon sent when the welcome screen's call-to-action is
* clicked. That button links straight out to the centrally hosted wizard,
* so there is no server-side moment at which we would otherwise notice the
* departure.
*
* `charitable_started_onboarding` is what `resume_onboarding()` reads to
* decide someone is mid-setup and pull them back to the welcome screen. It
* used to be set on activation, which meant every new install was treated
* as mid-setup whether or not the user ever went to the wizard. Setting it
* here instead makes the option mean what its name says.
*
* Deliberately best-effort: if the beacon does not arrive (JavaScript
* disabled, browser without sendBeacon, request dropped mid-navigation) the
* user still reaches the wizard normally and simply is not offered the
* resume prompt afterwards. Losing a prompt is an acceptable failure; a
* broken call-to-action on the first screen of a new install is not.
*
* @since 1.8.12.4
*
* @return void
*/
public function ajax_mark_onboarding_started() {
check_ajax_referer( 'charitable_onboarding_started', 'nonce' );
if ( ! charitable_current_user_can( 'administrator' ) ) {
wp_send_json_error( null, 403 );
}
update_option( 'charitable_started_onboarding', 1 );
wp_send_json_success();
}
/**
* Onboarding welcome screen redirect.
*
* This function checks if a new install or update has just occurred. If so,
* then we redirect the user to the appropriate page.
*
* @since 1.8.1.12
* @version 1.8.3 tweak what is an initial install.
* @version 1.8.4 add charitable_started_onboarding option.
* @version 1.8.9.2 Added WordPress filter and context awareness.
*/
public function redirect() {
// Don't redirect during WordPress maintenance operations.
if ( defined( 'DOING_AUTOSAVE' ) || wp_is_json_request() ||
defined( 'IFRAME_REQUEST' ) || ( function_exists( 'wp_doing_heartbeat' ) && wp_doing_heartbeat() ) ) {
return;
}
// Check if we should consider redirection.
if ( ! get_transient( 'charitable_activation_redirect' ) ) {
return;
}
// If we are redirecting, clear the transient so it only happens once.
delete_transient( 'charitable_activation_redirect' );
// Check option to disable welcome redirect.
if ( get_option( 'charitable_activation_redirect', false ) ) {
return;
}
// Allow developers to disable activation redirect via filter.
if ( apply_filters( 'charitable_disable_activation_redirect', false ) ) {
return;
}
// Only do this for single site installs.
if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { // phpcs:ignore WordPress.Security.NonceVerification
return;
}
// Check if this is an update or first install.
$upgrade = get_option( 'charitable_version_upgraded_from', [] );
if ( ! $upgrade || 1 === count( $upgrade ) ) {
// Initial install.
//
// NOTE: `charitable_started_onboarding` is deliberately NOT set here.
// Being redirected to the welcome screen is not the same as having
// started the wizard, and `resume_onboarding()` treats that option as
// "this user is mid-setup" and redirects them back here from other
// admin pages until it is cleared. Setting it on activation therefore
// nagged every new install, including people who never clicked
// through. It is now set only when the user actually leaves for the
// wizard - see `ajax_mark_onboarding_started()`.
wp_safe_redirect( admin_url( 'admin.php?page=charitable&wpchar_lite=lite&setup=welcome&f=1' ) );
exit;
}
}
/**
* Enqueue assets.
*
* @since 1.8.4
*/
public function enqueue_scripts() {
$min = charitable_get_min_suffix();
$version = charitable()->get_version();
$assets_dir = charitable()->get_path( 'assets', false );
/* The following styles are only loaded on Charitable screens. */
$screen = get_current_screen();
if ( $this->is_setup_page() ) {
wp_enqueue_script(
'campaign-admin-congrats-wizard-confetti',
charitable()->get_path( 'directory', false ) . 'assets/js/libraries/confetti.min.js',
[ 'jquery' ],
$version,
true
);
}
if ( $this->is_setup_page() || $this->is_welcome_page() ) {
wp_register_style(
'charitable-admin-user-onboarding',
$assets_dir . 'css/admin/charitable-admin-user-onboarding' . $min . '.css',
array(),
$version
);
wp_enqueue_style( 'charitable-admin-user-onboarding' );
if ( $this->is_welcome_page() ) :
/*
* The welcome screen's call-to-action links straight out to the
* centrally hosted wizard, so nothing server-side observes the
* departure. Send a beacon on click so
* `charitable_started_onboarding` is set only for users who
* genuinely leave for the wizard, rather than for everyone who
* merely activated the plugin.
*
* Inline rather than a new asset file: it is a few lines, the
* existing onboarding script is only registered on the setup page
* and pulls in the confetti library, and a new file would need a
* minified twin to match the $min convention here.
*/
wp_register_script( 'charitable-onboarding-departure', '', array(), $version, true );
wp_enqueue_script( 'charitable-onboarding-departure' );
wp_add_inline_script(
'charitable-onboarding-departure',
'(function(){' .
'var d=' . wp_json_encode(
array(
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'charitable_onboarding_started' ),
)
) . ';' .
'document.addEventListener("DOMContentLoaded",function(){' .
'var a=document.querySelector(".charitable-user-onboarding-wrap a.charitable-button-link");' .
'if(!a){return;}' .
'a.addEventListener("click",function(){' .
'try{' .
'var f=new FormData();' .
'f.append("action","charitable_onboarding_started");' .
'f.append("nonce",d.nonce);' .
'if(navigator.sendBeacon){navigator.sendBeacon(d.url,f);}' .
'else{fetch(d.url,{method:"POST",body:f,credentials:"same-origin",keepalive:true}).catch(function(){});}' .
'}catch(e){}' .
'});' . // Never preventDefault: navigation to the wizard must proceed regardless.
'});' .
'})();'
);
endif;
if ( $this->is_setup_page() ) :
wp_register_script(
'charitable-admin-user-onboarding',
$assets_dir . 'js/admin/charitable-admin-user-onboarding' . $min . '.js',
array( 'jquery', 'campaign-admin-congrats-wizard-confetti' ),
$version,
true
);
wp_enqueue_script( 'charitable-admin-user-onboarding' );
wp_localize_script(
'charitable-admin-user-onboarding',
'charitable_setup',
$this->get_localized_strings()
);
endif;
}
}
/**
* Is this the setup page?
*
* @since 1.8.4
*
* @return bool
*/
public function is_setup_page() {
return is_admin() && isset( $_GET['page'] ) && 'charitable-setup' === $_GET['page']; // phpcs:ignore
}
/**
* Is this the welcome page?
*
* @since 1.8.4
*
* @return bool
*/
public function is_welcome_page() {
return is_admin() && isset( $_GET['page'] ) && 'charitable' === $_GET['page'] && isset( $_GET['wpchar_lite'] ) && 'lite' === $_GET['wpchar_lite'] && isset( $_GET['setup'] ) && ( 'cancelled' === $_GET['setup'] || 'welcome' === $_GET['setup'] || 'return' === $_GET['setup'] ); // phpcs:ignore
}
/**
* Maybe process the next step.
*
* @since 1.8.4
*
* @return void
*/
public function maybe_process_step() {
// security check.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-setup' ) ) { // phpcs:ignore
wp_send_json_error( array( 'message' => 'Invalid nonce.' ) );
}
$current_step = (string) $this->get_current_setup_step();
if ( 'meta' === $current_step ) {
$meta = get_option( 'charitable_setup_meta', array() );
$processed_meta = $this->process_meta( $meta );
// we are also going to process the payment options (enable them, leaving a possible Stripe connect for later near the end).
$payment_methods = $this->retrieve_payment_methods();
$processed_pm = $this->process_payment_methods( $payment_methods );
}
// return json success.
wp_send_json_success(
array(
'status' => 'success',
'processed_step' => $current_step,
'next_step' => $this->get_next_step(),
)
);
exit;
}
/**
* Get the next setup step by looking at the private $steps property and returning the next value.
*
* @param string $current_step The current step.
*
* @since 1.8.4
*
* @return string
*/
public function get_next_step( $current_step = '' ) {
if ( ! $current_step ) {
$current_step = $this->get_current_setup_step();
}
$steps = $this->steps;
$next_step = '';
foreach ( $steps as $key => $step ) {
if ( $step === $current_step ) {
$next_step = $steps[ $key + 1 ];
break;
}
}
return $next_step;
}
/**
* Get the current setup step.
* Possible values: meta, plugins, payment_methods, features.
*
* @since 1.8.4
*
* @return int
*/
public function get_current_setup_step() {
return 'meta';
}
/**
* Register the page.
*
* @since 1.3.0
*
* @return void
*/
public function register_page() {
add_dashboard_page(
__( 'Setup Charitable', 'charitable' ),
__( 'Setup Charitable', 'charitable' ),
'manage_charitable_settings',
'charitable-setup',
array( $this, 'render_page' )
);
}
/**
* Remove the page from the dashboard menu.
*
* @since 1.3.0
*
* @return void
*/
public function remove_page_from_menu() {
remove_submenu_page( 'index.php', 'charitable-setup' );
}
/**
* Render the page.
*
* @since 1.3.0
*
* @return void
*/
public function render_page() {
$stripe_connect_url = '';
$stripe_connected = false;
$stripe_returned = isset( $_GET['setup'] ) && 'stripe-connect' === $_GET['setup'] ? true : false; // phpcs:ignore
if ( ! $stripe_returned ) {
$payment_methods = $this->retrieve_payment_methods();
if ( in_array( 'stripe', $payment_methods, true ) && class_exists( 'Charitable_Gateway_Stripe_AM' ) ) {
$stripe_gateway = new Charitable_Gateway_Stripe_AM();
$stripe_connect_url = ! $stripe_gateway->maybe_stripe_connected() ? $stripe_gateway->get_stripe_connect_url( $this->stripe_redirect_url ) : '';
$stripe_connected = $stripe_gateway->maybe_stripe_connected();
}
}
// The "continue" or "return" URL should the checklist by default, but on the chance the checklist is completed, we'll send them to the dashboard.
$checklist_class = Charitable_Checklist::get_instance();
$checklist_possible = $checklist_class->is_checklist_skipped() || $checklist_class->is_checklist_completed();
$completed_url = $checklist_possible ? admin_url( 'admin.php?page=charitable-dashboard' ) : admin_url( 'admin.php?page=charitable-setup-checklist' );
// If the checklist is not an option, check and see if they created a campaign. If so, send them to that draft campaign.
$campaign_admin_url = false !== get_option( 'charitable_setup_campaign_created', false )
? admin_url( 'admin.php?page=charitable-campaign-builder&campaign_id=' . intval( get_option( 'charitable_setup_campaign_created', 0 ) ) . '&view=design' )
: false;
charitable_admin_view(
'onboarding/setup',
array(
'page' => $this,
'stripe_connect_url' => $stripe_connect_url,
'stripe_connected' => $stripe_connected,
'stripe_returned' => $stripe_returned,
'completed_url' => $completed_url,
'campaign_admin_url' => $campaign_admin_url,
)
);
}
/**
* Store the meta.
*
* @param array $meta The meta.
*
* @since 1.8.4
*/
public function store_meta( $meta = array() ) {
update_option( 'charitable_setup_meta', $meta, false );
}
/**
* Store plugin data.
*
* @param array $plugins The plugins.
*
* @since 1.8.4
*/
public function store_plugins( $plugins = array() ) {
update_option( 'charitable_setup_plugins', $plugins, false );
}
/**
* Retrieve the plugin data that was stored.
*
* @since 1.8.4
*
* @return array
*/
public function retrieve_plugins() {
$plugins = get_option( 'charitable_setup_plugins', array() );
if ( ! is_array( $plugins ) ) {
return array();
}
$plugins = array_map( 'sanitize_text_field', $plugins );
if ( empty( $plugins ) ) {
return array();
}
return $plugins;
}
/**
* Return plugins that need to be installed potentially.
*
* @since 1.8.4
*
* @return array
*/
public function plugins_to_install() {
$plugins = $this->retrieve_plugins();
if ( empty( $plugins ) ) {
return array();
}
$plugins = array_map(
function ( $plugin ) {
// is this plugin already installed in the WordPress site?
if ( ! $this->is_plugin_installed( $plugin ) ) {
return $plugin;
}
},
$plugins
);
// remove any null values and remove keys.
$plugins = array_values( array_filter( $plugins ) );
return $plugins;
}
/**
* Return plugins that need to be activated potentially.
*
* @since 1.8.4
*
* @return array
*/
public function plugins_to_activate() {
$plugins = $this->retrieve_plugins();
if ( empty( $plugins ) ) {
return array();
}
// check if the plugin is installed and activated. If not, add it to the list of plugins to activate.
$plugins = array_map(
function ( $plugin ) {
// is this plugin already installed in the WordPress site?
if ( $this->is_plugin_installed( $plugin ) ) {
// is this plugin already activated?
if ( ! is_plugin_active( $plugin ) ) {
return $plugin;
}
}
},
$plugins
);
// remove any null values and reindex the array.
$plugins = array_values( array_filter( $plugins ) );
return $plugins;
}
/**
* Returns stored "features".
*
* @since 1.8.4
*
* @return array
*/
public function features_to_install() {
$features = get_option( 'charitable_setup_features', array() );
if ( ! is_array( $features ) ) {
return array();
}
$features = array_map( 'sanitize_text_field', $features );
if ( empty( $features ) ) {
return array();
}
return $features;
}
/**
* Activate "features" via ajax call.
*
* @since 1.8.4
*
* @return void
*/
public function ajax_activate_feature() {
// security check.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-admin' ) ) { // phpcs:ignore
wp_send_json_error( array( 'message' => 'Invalid nonce.' ) );
}
$plugin_basename = isset( $_POST['slug'] ) ? 'charitable-' . sanitize_text_field( $_POST['slug'] ) : ''; // phpcs:ignore
if ( ! $plugin_basename ) {
wp_send_json_error( array( 'message' => 'Invalid feature.' ) );
}
// override a few slug/names here.
if ( $plugin_basename === 'charitable-recurring' ) {
$plugin_basename = 'charitable-recurring-donations';
}
$is_pro = charitable_is_pro();
if ( ! $is_pro ) {
wp_send_json_error(
array(
'message' => 'Invalid access.',
'is_pro' => $is_pro,
'error' => 'test',
)
);
}
$plugin_slug = $plugin_basename . '/' . $plugin_basename . '.php';
// is this plugin already installed?
if ( is_plugin_active( $plugin_basename ) ) {
wp_send_json_success(
array(
'message' => 'Already installed and activated',
'error' => 'feature-plugin-success',
)
);
exit;
}
// is this plugin installed and just needs to be activated?
if ( $this->is_plugin_installed( $plugin_slug ) ) {
$activate = activate_plugin( $plugin_slug );
if ( is_wp_error( $activate ) ) {
wp_send_json_error(
array(
'message' => $activate->get_error_message(),
'error' => 'feature-plugin-error',
)
);
exit;
} else {
wp_send_json_success(
array(
'message' => "Plugin '{$plugin_slug}' activated successfully.",
'error' => 'feature-plugin-success',
)
);
exit;
}
}
// ok, so it's not installed at all? Let's install it - and activate it.
// check if the feature is already installed and if not install it.
$addons = charitable_get_addons_data_from_server();
$download_url = '';
if ( $addons ) {
foreach ( (array) $addons as $i => $addon ) {
if ( ! isset( $addon['slug'] ) || $addon['slug'] === '' || strtolower( $addon['slug'] ) === 'auto draft' ) {
continue;
}
if ( $addon['slug'] === $plugin_basename ) {
$addon_slug = $addon['slug'];
$download_url = ! empty( $addon['install'] ) ? esc_url( $addon['install'] ) : false;
break;
}
}
}
if ( ! $download_url ) {
wp_send_json_error(
array(
'message' => 'Feature not found.',
'error' => 'feature-not-found',
)
);
}
// we need to convery & to & in the download_url.
$download_url = str_replace( '&', '&', $download_url );
// Prepare variables.
$method = '';
$url = add_query_arg(
array(
'page' => 'charitable-settings',
),
admin_url( 'admin.php' )
);
$url = esc_url( $url );
// Start output bufferring to catch the filesystem form if credentials are needed.
ob_start();
$creds = request_filesystem_credentials( $url, $method, false, false, null );
if ( false === $creds ) {
$form = ob_get_clean();
echo wp_json_encode( array( 'form' => $form ) );
die;
}
// If we are not authenticated, make it happen now.
if ( ! WP_Filesystem( $creds ) ) {
ob_start();
request_filesystem_credentials( $url, $method, true, false, null );
$form = ob_get_clean();
echo wp_json_encode( array( 'form' => $form ) );
die;
}
// Do not allow WordPress to search/download translations, as this will break JS output.
remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 );
// We do not need any extra credentials if we have gotten this far, so let's install the plugin.
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once plugin_dir_path( CHARITABLE_DIRECTORY_PATH ) . 'charitable/includes/utilities/Skin.php';
// Create the plugin upgrader with our custom skin.
$skin = new Charitable_Skin();
$installer = new Plugin_Upgrader( $skin );
$installer->install( $download_url );
// Flush the cache and return the newly installed plugin basename.
wp_cache_flush();
if ( $installer->plugin_info() ) {
$plugin_basename = $installer->plugin_info();
$plugin = isset( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : '';
// attempt to activate the installed addon, save the user a step.
$activate = activate_plugins( $plugin_basename );
if ( is_plugin_active( $plugin_basename ) ) {
wp_send_json_success(
array(
'message' => "Plugin '{$plugin_slug}' activated successfully.",
'error' => 'feature-plugin-success',
)
);
} else {
wp_send_json_error(
array(
'message' => "Failed to activate plugin '{$plugin_slug}'.",
'error' => 'feature-plugin-error',
)
);
}
exit;
} else {
wp_send_json_error(
array(
'message' => "Failed issue with installing plugin '{$plugin_slug}'.",
'error' => 'feature-plugin-error',
)
);
}
}
/**
* Get license key.
*
* @param string $license_key The license key.
*
* @since 1.8.4
*/
public function get_license_key( $license_key = '' ) { // phpcs:ignore
return get_option( 'charitable_setup_license_key', '' );
}
/**
* Store license key.
*
* @param string $license_key The license key.
*
* @since 1.8.4
*/
public function store_license_key( $license_key = '' ) {
update_option( 'charitable_setup_license_key', $license_key, '' );
}
/**
* Store payment methods.
*
* @param array $payment_methods The payment methods.
*
* @since 1.8.4
*/
public function store_payment_methods( $payment_methods = array() ) {
update_option( 'charitable_setup_payment_methods', $payment_methods, false );
}
/**
* Store features.
*
* @param array $features The features.
*
* @since 1.8.4
*/
public function store_features( $features = array() ) {
update_option( 'charitable_setup_features', $features, false );
}
/**
* Store campaign info for setup.
*
* @param array $campaign The campaign.
* @since 1.8.4
*/
public function store_campaign( $campaign = array() ) {
update_option( 'charitable_setup_campaign', $campaign, false );
}
/**
* Determines if we can skip building a new campaign.
*
* @since 1.8.4
*
* @return bool
*/
public function maybe_skip_campaign_setup() {
// Check for permissions.
if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
return false;
}
$campaign_data = $this->retrieve_campaign();
if ( ! $campaign_data ) {
return true;
}
return false;
}
/**
* Processing opt-in tracking requests.
*
* @since 1.8.4
*
* @return void
*/
public function ajax_process_tracking() {
// security check.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-admin' ) ) { // phpcs:ignore
wp_send_json_error(
array(
'message' => 'Invalid nonce.',
'error' => 'invalid-nonce',
)
);
}
// Check for permissions.
if ( ! charitable_current_user_can( 'administrator' ) ) {
wp_send_json_error(
array(
'message' => 'You are not allowed to perform this action.',
'error' => 'permission-denied',
)
);
}
// checking for the action.
if ( ! isset( $_POST['action'] ) ) {
wp_send_json_error(
array(
'message' => 'Invalid tracking action.',
'error' => 'invalid-tracking-action',
)
);
}
if ( ! isset( $_POST['tracking_action'] ) || ! in_array( $_POST['tracking_action'], array( 'opt-in', 'opt-out' ), true ) ) {
wp_send_json_error(
array(
'message' => 'Invalid tracking action.',
'error' => 'invalid-tracking-action',
)
);
}
// setup variables.
$opt_in_tracking = empty( $_POST['tracking_action'] ) || $_POST['tracking_action'] === 'opt-out' ? '0' : '1';
$email = update_option( 'charitable_email_signup', false ) ? get_option( 'charitable_email_signup' ) : get_option( 'admin_email' );
// assemble a url to the setup wizard using WordPress function.
$tracking_update_url = add_query_arg(
array(
'email' => is_email( $email ) ? $email : false,
'opt_in_track' => $opt_in_tracking,
'site_token' => charitable_get_site_token(),
'return' => rawurlencode(
base64_encode( get_admin_url( null, 'admin.php') ) // phpcs:ignore
),
),
charitable_onboarding_app_url() . '/optin-charitable_tracking'
);
// Send the POST request.
$response = wp_remote_get(
$tracking_update_url,
[
'timeout' => 45,
'sslverify' => false,
]
);
// Get the http status code.
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $response_code ) {
wp_send_json_error(
array(
'message' => 'Error updating tracking.',
'error' => 'tracking-error',
)
);
exit;
}
if ( is_wp_error( $response ) ) {
// Handle the error.
$error_message = $response->get_error_message();
wp_send_json_error(
array(
'message' => 'Error updating tracking: ' . $error_message,
'error' => 'tracking-error',
)
);
exit;
}
$response_args = json_decode( $response['body'] );
$return_url = isset( $response_args->return_url ) ? $response_args->return_url : false;
$site_token = isset( $response_args->site_token ) ? $response_args->site_token : false;
$opt_in_tracking = isset( $response_args->opt_in_tracking ) ? intval( $response_args->opt_in_tracking ) : false;
// the site token should match the site token we have.
if ( $site_token !== charitable_get_site_token() ) {
wp_send_json_error(
array(
'message' => 'Error updating tracking: Invalid site token.',
'error' => 'tracking-error',
)
);
exit;
}
// flip the $opt_in_tracking variable... if it's opt-in, we want to opt-out and vice versa.
$opt_in_tracking_setting = ( 0 === intval( $opt_in_tracking ) ) ? false : true;
charitable_update_usage_tracking_setting( $opt_in_tracking_setting );
// redirect to the return url.
wp_send_json_success(
array(
'text' => $opt_in_tracking === 0 ? 'Allow And Continue' : 'Opt Out',
'return' => $return_url,
'tracking_action' => $opt_in_tracking === 0 ? '' : 'joined',
'opt_in_tracking' => $opt_in_tracking === 0 ? false : true,
)
);
exit;
}
/**
* Process campaign.
*
* @since 1.8.4
*
* @return void
*/
public function ajax_process_campaign() {
// security check.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-setup' ) ) { // phpcs:ignore
wp_send_json_error(
array(
'message' => 'Invalid nonce.',
'error' => 'invalid-nonce',
)
);
}
// Check for permissions.
if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
wp_send_json_error(
array(
'message' => 'You are not allowed to perform this action.',
'error' => 'permission-denied',
)
);
}
$campaign_data = $this->retrieve_campaign();
if ( ! $campaign_data ) {
wp_send_json_error(
array(
'message' => 'No campaign.',
'error' => 'invalid-campaign',
'processed_campaign' => 'false',
)
);
}
// Remove any previous one, if it exists.
delete_option( 'charitable_setup_campaign_created' );
$campaign_template = isset( $campaign_data['template'] ) ? sanitize_text_field( $campaign_data['template'] ) : ''; // example: animal-sanctuary.
$campaign_title = isset( $campaign_data['title'] ) ? sanitize_text_field( $campaign_data['title'] ) : '';
$campaign_description = isset( $campaign_data['description'] ) ? sanitize_text_field( $campaign_data['description'] ) : '';
$campaign_goal = isset( $campaign_data['goal'] ) ? sanitize_text_field( $campaign_data['goal'] ) : '0';
// if $campaign_end_date is a validated date, then we can convert it to a proper date format "2025-01-25 23:59:59". If it's not a valid date, then we can set it to 0.
$campaign_end_date = isset( $campaign_data['end_date'] ) ? sanitize_text_field( $campaign_data['end_date'] ) : 0;
$campaign_end_date = strtotime( $campaign_end_date ) ? gmdate( 'Y-m-d H:i:s', strtotime( $campaign_end_date ) ) : 0;
// Generate the campaign in the datbase so we can get the database ID.
$campaign_id = charitable_create_campaign(
array(
'title' => $campaign_title,
'content' => $campaign_description,
'creator' => get_current_user_id(),
'end_date' => $campaign_end_date,
'goal' => $campaign_goal,
'status' => 'draft',
)
);
// Process the campaign, which has it's roots back to legacy campaigns.
$processor = new Charitable_Campaign_Processor();
$processor->campaign_id = $campaign_id;
// taken from class-charitable-campaign-processor.php.
$campaign_meta_keys = array(
'goal' => $campaign_goal,
'end_date' => $campaign_end_date,
);
foreach ( $campaign_meta_keys as $meta_key => $meta_value ) {
$processor->save_meta_field( $meta_value, $meta_key );
}
$campaign_data = array(
'title' => $campaign_title,
);
// get the template settings.
$builder_template = new Charitable_Campaign_Builder_Templates();
$template_settings = $builder_template->get_template_data( $campaign_template, $campaign_data );
// Update (overwrite) the campaign settings with the new values.
$campaign_settings_v2 = [];
$campaign_settings_v2['layout'] = $this->transform_layout_structure( $template_settings['layout'] ); // Updated.
$campaign_settings_v2['id'] = $campaign_id;
$campaign_settings_v2['template_id'] = $template_settings['meta']['slug'];
$campaign_settings_v2['template_label'] = $template_settings['meta']['label'];
$campaign_settings_v2['color_base_primary'] = $template_settings['meta']['colors']['primary'];
$campaign_settings_v2['color_base_secondary'] = $template_settings['meta']['colors']['secondary'];
$campaign_settings_v2['color_base_tertiary'] = $template_settings['meta']['colors']['tertiary'];
$campaign_settings_v2['color_base_button'] = $template_settings['meta']['colors']['button_bg'];
$campaign_settings_v2['layout']['advanced']['theme_color_primary'] = $template_settings['meta']['colors']['primary'];
$campaign_settings_v2['layout']['advanced']['theme_color_secondary'] = $template_settings['meta']['colors']['secondary'];
$campaign_settings_v2['layout']['advanced']['theme_color_tertiary'] = $template_settings['meta']['colors']['tertiary'];
$campaign_settings_v2['layout']['advanced']['theme_color_button'] = $template_settings['meta']['colors']['button_bg'];
$campaign_settings_v2['layout']['advanced']['tab_style'] = $template_settings['advanced']['tab_style'];
$campaign_settings_v2['layout']['advanced']['tab_size'] = $template_settings['advanced']['tab_size'];
$campaign_settings_v2['layout']['advanced']['show_field_names'] = 'hide'; // $template_settings['advanced']['show_field_names'];
$campaign_settings_v2['layout']['advanced']['preview_mode'] = $template_settings['advanced']['preview_mode'];
$campaign_settings_v2['post_status'] = 'draft';
$campaign_settings_v2['post_status_label'] = 'Draft';
$campaign_settings_v2['title'] = $campaign_title;
$campaign_settings_v2['fields'] = $this->campaign_map_fields( $template_settings );
$campaign_settings_v2['tabs'] = $this->get_tabs_from_template_for_campaign_settings( $template_settings );
$campaign_settings_v2['field_id'] = count( $campaign_settings_v2['fields'] ); // ?????
$campaign_settings_v2['settings'] = [];
$campaign_settings_v2['settings']['general']['description'] = $campaign_description;
$campaign_settings_v2['settings']['general']['extended_description'] = '';
$campaign_settings_v2['settings']['general']['form_css_class'] = '';
$campaign_settings_v2['settings']['general']['goal'] = '0' === $campaign_goal ? '' : Charitable_Currency::get_instance()->sanitize_monetary_amount( (string) ( $campaign_goal ) );
$campaign_settings_v2['settings']['general']['end_date'] = Charitable_Campaign::sanitize_campaign_end_date( $campaign_end_date );
$campaign_settings_v2['settings']['general']['campaign_creator_id'] = get_current_user_id();
$campaign_settings_v2['settings']['donation-options'] = $this->get_donation_options();
$campaign_settings_v2['campaign_id'] = $campaign_id;
$campaign_settings_v2['tab_order'] = array(
0 => 0,
1 => 1,
2 => 2,
);
// Allow addons to update, sync, or override settings before they are saved.
$campaign_settings_v2 = apply_filters( 'charitable_campaign_builder_save_campaign_settings', $campaign_settings_v2, $campaign_id );
// Save the campaign settings.
$updated = update_post_meta( $campaign_id, 'campaign_settings_v2', $campaign_settings_v2 );
if ( ! $updated ) {
wp_send_json_error(
array(
'message' => 'Error saving campaign settings.',
'error' => 'error-saving-campaign-settings',
)
);
} else {
// Create a (temp) option that marks the creation of the campaign during the setup process so we can direct the user back to the campaign.
update_option( 'charitable_setup_campaign_created', $campaign_id, false );
if ( class_exists( 'Charitable_Checklist' ) ) {
$checklist_class = Charitable_Checklist::get_instance();
$checklist_class->mark_step_completed( 'first-campaign' );
}
wp_send_json_success(
array(
'message' => 'Campaign created.',
'status' => 'campaign-created',
)
);
}
exit;
}
/**
* Convert layout structure to the new format.
*
* @since 1.8.4
* @param array $original_layout The original layout.
*
* @return array
*/
public function transform_layout_structure( $original_layout ) {
$new_layout = [
'rows' => [],
];
$field_counter = 0;
$field_type_counter = 0;
$section_counter = 0;
$columns_counter = 0;
$alt_index_starter = 0;
foreach ( $original_layout as $row_index => $row ) {
if ( ! is_numeric( $row_index ) ) {
continue;
}
$new_row = [
'type' => $row['type'],
'css_class' => $row['css_class'] ?? 'no-css',
'columns' => [],
'fields' => [],
];
$alt_number = 0;
// Process columns.
foreach ( $row['columns'] as $column_index => $column ) {
$new_column = [
'sections' => [],
];
foreach ( $column as $section ) {
if ( $section['type'] === 'fields' ) {
$new_section = [
'type' => 'fields',
'fields' => [],
];
$field_indices = [];
foreach ( $section['fields'] as $field ) {
// Add to row's fields array.
$new_row['fields'][ $field_type_counter ] = $field['type'];
// Store the index for this field.
$field_indices[] = $field_counter;
++$field_counter;
++$field_type_counter;
}
$alt_index_starter = count( $new_row['fields'] );
$new_section['fields'] = $field_indices;
$new_column['sections'][ $section_counter ] = $new_section;
++$section_counter;
} elseif ( $section['type'] === 'tabs' ) {
$new_section = [
'type' => 'tabs',
'tabs' => [],
];
foreach ( $section['tabs'] as $tab ) {
$new_tab = [
'title' => $tab['title'],
'type' => 'fields',
'slug' => $tab['slug'] ?? '',
'fields' => [],
];
$field_indices = [];
if ( ! empty( $tab['fields'] ) ) {
foreach ( $tab['fields'] as $field ) {
// Add to row's fields array.
$new_row['fields'][ $field_type_counter ] = $field['type'];
// Store the index for this field.
$field_indices[] = $field_counter;
++$field_counter;
++$field_type_counter;
}
}
$new_tab['fields'] = $field_indices;
$new_section['tabs'][] = $new_tab;
}
$new_column['sections'][ $section_counter ] = $new_section;
++$section_counter;
}
}
$new_row['columns'][ $columns_counter ] = $new_column;
++$columns_counter;
}
$new_layout['rows'][] = $new_row;
}
return $new_layout;
}
/**
* Get the tabs from the template for the campaign settings.
*
* @param array $template_settings The template settings.
*
* @since 1.8.4
* @version 1.8.9.7
*
* @return array
*/
public function get_tabs_from_template_for_campaign_settings( $template_settings ) {
$tabs = [];
foreach ( $template_settings['layout'] as $row ) {
foreach ( $row as $column ) {
if ( ! is_array( $column ) ) {
continue;
}
foreach ( $column as $section ) {
if ( ! is_array( $section ) ) {
continue;
}
foreach ( $section as $mini_sections ) {
if ( ! is_array( $mini_sections ) || ! isset( $mini_sections['tabs'] ) ) {
continue;
}
foreach ( $mini_sections['tabs'] as $found_tabs ) {
$tabs[] = $found_tabs;
}
}
}
}
}
$returned_tabs = [];
foreach ( $tabs as $index => $tab ) {
$returned_tabs[] = array(
'title' => isset( $tab['title'] ) ? $tab['title'] : '',
);
}
return $returned_tabs;
}
/**
* Map the "fields" option in the root of the array from the template to the campaign settings.
*
* @since 1.8.4
* @param array $input The input (campaign array).
*
* @return array
*/
public function campaign_map_fields( $input ) {
$output = array( 'fields' => array() );
// Check if 'layout' exists and is an array.
if ( isset( $input['layout'] ) && is_array( $input['layout'] ) ) {
foreach ( $input['layout'] as $layout_item ) {
// Check if 'columns' exists and is an array.
if ( isset( $layout_item['columns'] ) && is_array( $layout_item['columns'] ) ) {
foreach ( $layout_item['columns'] as $column ) {
if ( is_array( $column ) ) {
foreach ( $column as $column_item ) {
// Check if 'fields' exists and is an array.
if ( isset( $column_item['fields'] ) && is_array( $column_item['fields'] ) ) {
foreach ( $column_item['fields'] as $field ) {
// Process each field.
$mapped_field = $this->process_field( $field );
$output['fields'][] = $mapped_field;
}
} elseif ( isset( $column_item['tabs'] ) && is_array( $column_item['tabs'] ) ) { // Check if 'tabs' exists (e.g., in the second layout item).
foreach ( $column_item['tabs'] as $tab ) {
if ( isset( $tab['fields'] ) && is_array( $tab['fields'] ) ) {
foreach ( $tab['fields'] as $field ) {
$mapped_field = $this->process_field( $field );
$output['fields'][] = $mapped_field;
}
}
}
}
}
}
}
}
}
}
return $output['fields'];
}
/**
* Process the field from the campaign_map_fields function.
*
* @since 1.8.4
* @param array $field The field.
*
* @return array
*/
public function process_field( $field ) {
$mapped_field = array();
// Get the field type.
$type = isset( $field['type'] ) ? $field['type'] : '';
// Map fields based on the type.
switch ( $type ) {
case 'photo':
$mapped_field['file'] = isset( $field['file'] ) ? $field['file'] : '';
$mapped_field['alt_text'] = isset( $field['alt_text'] ) ? $field['alt_text'] : '';
$mapped_field['default'] = isset( $field['default'] ) ? $field['default'] : '';
$mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
$mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'center';
$mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
break;
case 'campaign-description':
case 'text':
$mapped_field['headline'] = isset( $field['headline'] ) ? $field['headline'] : '';
$mapped_field['content'] = isset( $field['content'] ) ? $field['content'] : '';
$mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
$mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'left';
$mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
break;
case 'progress-bar':
case 'campaign-summary':
$mapped_field['headline'] = isset( $field['headline'] ) ? $field['headline'] : '';
$mapped_field['show_hide'] = array();
if ( isset( $field['show_hide'] ) ) {
foreach ( $field['show_hide'] as $key => $value ) {
if ( $value ) {
$mapped_field['show_hide'][ $key ] = $key;
}
}
}
$mapped_field['meta_position'] = isset( $field['meta_position'] ) ? $field['meta_position'] : '';
$mapped_field['label_donate'] = isset( $field['label_donate'] ) ? $field['label_donate'] : '';
$mapped_field['label_goal'] = isset( $field['label_goal'] ) ? $field['label_goal'] : '';
$mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
$mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'left';
$mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
break;
case 'donate-button':
$mapped_field['button_label'] = isset( $field['button_label'] ) ? $field['button_label'] : '';
$mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
$mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'left';
$mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
break;
default:
// If the type is unrecognized, you can decide how to handle it.
$mapped_field = $field;
break;
}
return $mapped_field;
}
/**
* Setup default donation options ready for the campaign.
*
* @since 1.8.4
*
* @return array
*/
public function get_donation_options() {
return [
'donation_amounts' => [
1 => [
'amount' => 5,
'description' => 'This is a small donation.',
],
2 => [
'amount' => 10,
'description' => 'This is a slightly larger donation.',
],
3 => [
'amount' => 15,
'description' => 'This is a larger (and default) donation.',
],
4 => [
'amount' => 20,
'description' => 'This is a large donation.',
],
],
'suggested_donations_default' => 3,
'minimum_donation_amount' => null,
'allow_custom_donations' => 1,
];
}
/**
* Clean up and mark the setup as complete.
*
* @since 1.8.4
* @version 1.8.5 - Added charitable_activate_pro option.
*
* @return void
*/
public function ajax_process_complete() {
// security check.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-setup' ) ) { // phpcs:ignore
wp_send_json_error(
array(
'message' => 'Invalid nonce.',
'error' => 'invalid-nonce',
)
);
}
// CRITICAL SAFETY CHECK: Only set Pro activation if Pro plugin actually exists
$pro_plugin_path = WP_PLUGIN_DIR . '/charitable-pro/charitable-pro.php';
$should_activate_pro = file_exists( $pro_plugin_path );
// remove the transient, set the option.
delete_transient( 'charitable_ss_onboarding' );
update_option( 'charitable_ss_complete', true );
// Only set Pro activation if Pro is actually available
if ( $should_activate_pro ) {
update_option( 'charitable_activate_pro', true );
} else {
// Log this for debugging
if ( charitable_is_debug() ) {
error_log( 'Charitable: Pro plugin not found during setup completion - skipping Pro activation' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
}
wp_send_json_success(
array(
'message' => 'Setup completed.',
'status' => 'setup-completed',
)
);
}
/**
* Retrieve campaign.
*
* @since 1.8.4
*
* @return mixed
*/
public function retrieve_campaign() {
$campaign = get_option( 'charitable_setup_campaign', array() );
if ( ! is_array( $campaign ) ) {
return array();
}
$campaign = array_map( 'sanitize_text_field', $campaign );
$campaign = array_map( 'trim', $campaign );
if ( empty( $campaign ) ) {
return array();
}
// check and see if we have the vital fields (a campaign name and a template).
if ( ! key_exists( 'title', $campaign ) || ! key_exists( 'template', $campaign ) ) {
return false;
}
if ( empty( $campaign['title'] ) || empty( $campaign['template'] ) ) {
return false;
}
return $campaign;
}
/**
* Retrieve payment methods.
*
* @since 1.8.4
*
* @return mixed
*/
public function retrieve_payment_methods() {
$payment_methods = get_option( 'charitable_setup_payment_methods', array() );
if ( ! is_array( $payment_methods ) ) {
return array();
}
$payment_methods = array_map( 'sanitize_text_field', $payment_methods );
if ( empty( $payment_methods ) ) {
return array();
}
return $payment_methods;
}
/**
* Helper function to determine if a plugin is installed.
*
* @since 1.8.4
* @param string $plugin_slug The plugin slug.
*
* @return mixed
*/
public function is_plugin_installed( $plugin_slug ) {
// Get the full path to the plugins directory.
$plugin_dir = WP_PLUGIN_DIR . '/' . $plugin_slug;
// Check if the directory exists for the given plugin slug.
return is_dir( $plugin_dir );
}
/**
* Process meta.
*
* @since 1.8.4
* @version 1.8.4.2 - Upon successful tracking opt-in, send initial usage information.
*
* @param array $meta The meta to process.
* @return bool
*/
public function process_meta( $meta = array() ) {
if ( ! $meta ) {
return false;
}
$meta = array_map( 'sanitize_text_field', $meta );
// Define vars to determine if we can/should check off checklist items depending on what is updated from onboarding.
$checklist_general_settings_completed = false;
$checklist_email_settings_completed = false;
// step 2, country, currency, decimal count.
if ( key_exists( 'country', $meta ) ) {
$checklist_general_settings_completed = true;
$this->set_charitable_option( 'country', $meta['country'] );
}
if ( key_exists( 'currency', $meta ) ) {
$checklist_general_settings_completed = true;
$this->set_charitable_option( 'currency', $meta['currency'] );
}
if ( key_exists( 'decimal_count', $meta ) ) {
$checklist_general_settings_completed = true;
$this->set_charitable_option( 'decimal_count', $meta['decimal'] );
}
// step 2, email settings.
if ( key_exists( 'email_donor_notification', $meta ) ) {
$checklist_email_settings_completed = true;
$this->set_charitable_email_option( 'email_donor_notification' );
}
if ( key_exists( 'email_admin_notification', $meta ) ) {
$checklist_email_settings_completed = true;
$this->set_charitable_email_option( 'email_admin_notification' );
}
// If the general settings are completed, we can check off the checklist item.
if ( $checklist_general_settings_completed ) {
$checklist_class = Charitable_Checklist::get_instance();
$checklist_class->mark_step_completed( 'general-settings' );
}
if ( $checklist_email_settings_completed ) {
$checklist_class = Charitable_Checklist::get_instance();
$checklist_class->mark_step_completed( 'email-settings' );
}
// misc.
if ( key_exists( 'tracking', $meta ) ) {
if ( 'yes' === $meta['tracking'] ) {
if ( charitable_is_debug() ) {
error_log( 'tracking is enabled' ); // phpcs:ignore
}
charitable_update_usage_tracking_setting( true );
// Send initial usage information just once (after that scheduled), now that we have permission.
if ( class_exists( 'Charitable_Tracking' ) ) {
Charitable_Tracking::get_instance()->send_checkins( false, true );
}
}
}
if ( key_exists( 'email_signup', $meta ) ) {
// if the value is a valid santitized email address...
if ( is_email( $meta['email_signup'] ) ) {
if ( charitable_is_debug() ) {
error_log( 'email signup is valid' ); // phpcs:ignore
}
update_option( 'charitable_email_signup', $meta['email_signup'] );
}
}
return true;
}
/**
* Process plugins.
*
* @since 1.8.4
*
* @param array $plugins The plugins to process.
* @return void
*/
public function process_plugins( $plugins = array() ) {
if ( ! $plugins ) {
return;
}
$plugins = array_map( 'sanitize_text_field', $plugins );
return $plugins;
}
/**
* Process meta.
*
* @since 1.8.4
*
* @param array $payment_methods The methods to process.
* @return bool
*/
public function process_payment_methods( $payment_methods = array() ) {
if ( ! $payment_methods || ! class_exists( 'Charitable_Gateways' ) ) {
return false;
}
$ret = Charitable_Gateways::get_instance()->check_setup_payment_methods();
return $ret;
}
/**
* Update a Charitable option.
*
* @since 1.8.4
*
* @param string $setting The setting key.
* @param mixed $value The setting value.
* @return void
*/
private function set_charitable_option( $setting = '', $value = false ) {
$settings = get_option( 'charitable_settings', [] );
$settings[ $setting ] = $value;
if ( defined( 'CHARITABLE_DEBUG_SETUP' ) ) {
if ( charitable_is_debug() ) {
error_log( 'Charitable Setup: Updated ' . $setting . ' to ' . $value ); // phpcs:ignore
}
}
update_option( 'charitable_settings', $settings );
}
/**
* Process email setting.
*
* @since 1.8.4
*
* @param string $email_setting The email setting to process.
* @return void
*/
private function set_charitable_email_option( $email_setting = '' ) {
$settings = get_option( 'charitable_settings', [] );
$enabled_emails = ! empty( $settings['enabled_emails'] ) ? $settings['enabled_emails'] : [];
switch ( $email_setting ) {
case 'email_donor_notification':
if ( ! key_exists( 'donation_receipt', $enabled_emails ) ) {
$enabled_emails['donation_receipt'] = 'Charitable_Email_Donation_Receipt';
if ( defined( 'CHARITABLE_DEBUG_SETUP' ) ) {
if ( charitable_is_debug() ) {
error_log( 'Charitable Setup: Added donation_receipt to enabled_emails' ); // phpcs:ignore
}
}
}
break;
case 'email_admin_notification':
if ( ! key_exists( 'new_donation', $enabled_emails ) ) {
$enabled_emails['new_donation'] = 'Charitable_Email_New_Donation';
if ( defined( 'CHARITABLE_DEBUG_SETUP' ) ) {
if ( charitable_is_debug() ) {
error_log( 'Charitable Setup: Added new_donation to enabled_emails' ); // phpcs:ignore
}
}
}
break;
}
$settings['enabled_emails'] = $enabled_emails;
update_option( 'charitable_settings', $settings );
}
/**
* Get localized strings.
*
* @since 1.8.4
*
* @return array
*/
private function get_localized_strings() {
// $currency_helper = charitable_get_currency_helper();
$stripe_connect_url = '';
$payment_methods = $this->retrieve_payment_methods();
$stripe_returned = isset( $_GET['setup'] ) && 'stripe-connect' === $_GET['setup'] ? true : false; // phpcs:ignore
if ( in_array( 'stripe', $payment_methods, true ) && class_exists( 'Charitable_Gateway_Stripe_AM' ) ) {
$stripe_gateway = new Charitable_Gateway_Stripe_AM();
$redirect_url = $this->stripe_redirect_url;
$stripe_connect_url = ! $stripe_gateway->maybe_stripe_connected() ? $stripe_gateway->get_stripe_connect_url( $redirect_url ) : '';
}
$strings = array(
'version' => '1.8.4',
'setup_step' => $this->get_current_setup_step(),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'headlines' => array(
'start' => esc_html__( 'Starting...', 'charitable' ),
'meta' => esc_html__( 'Setting Up General Settings...', 'charitable' ),
'plugins' => esc_html__( 'Updating Plugins...', 'charitable' ),
'plugins_installed' => esc_html__( 'Installing Plugins...', 'charitable' ),
'plugins_activated' => esc_html__( 'Activating Plugins...', 'charitable' ),
'activate_license' => esc_html__( 'Setting Up Features...', 'charitable' ),
'features' => esc_html__( 'Setting Up Features...', 'charitable' ),
'campaign' => esc_html__( 'Setting Up Your First Campaign...', 'charitable' ),
'payment_methods' => esc_html__( 'Setting Up Payment Methods...', 'charitable' ),
'almost_complete' => esc_html__( 'Almost done!', 'charitable' ),
'complete' => esc_html__( 'Congratulations!', 'charitable' ),
),
'text' => array(
'start' => '',
'meta' => esc_html__( 'Updating country, currency, and related settings for you.', 'charitable' ),
'plugins' => esc_html__( 'Checking plugins...', 'charitable' ),
'plugins_installed' => '',
'plugins_activated' => '',
'activate_license' => esc_html__( 'Checking license and site information...', 'charitable' ),
'features' => esc_html__( 'Checking license and site information...', 'charitable' ),
'campaign' => esc_html__( 'Creating draft...', 'charitable' ),
'payment_methods' => esc_html__( 'Checking payment methods...', 'charitable' ),
'almost_complete' => esc_html__( 'Finishing things...', 'charitable' ),
'almost_complete_stripe' => esc_html__( 'Stripe offers a seamless and secure payment experience for both you and your donors.', 'charitable' ),
'complete' => esc_html__( 'Your Charitable install has been setup and is ready for you.', 'charitable' ),
),
'nonce' => wp_create_nonce( 'charitable-admin' ),
'setup_nonce' => wp_create_nonce( 'charitable-setup' ),
'plugins' => array(
'install' => $this->plugins_to_install(),
'activate' => $this->plugins_to_activate(),
),
'plugins_meta' => $this->plugin_meta,
'features' => $this->features_to_install(),
'key' => $this->get_license_key(),
'key_nonce' => wp_create_nonce( 'charitable_settings-options' ),
'features_meta' => $this->feature_meta,
/* translators: %1$s: List of requested features */
'features_message' => esc_html__( 'In order to install and activate some of the features you requested %1$s you need to activate your PRO license after setup is complete.', 'charitable' ),
'features_message_activate' => esc_html__( 'Activating license and installing addons...', 'charitable' ),
'payment_methods' => $payment_methods,
'stripe_connect_url' => $stripe_connect_url,
'campaign_skip' => $this->maybe_skip_campaign_setup(),
'checklist_url' => admin_url( 'admin.php?page=charitable-setup-checklist' ),
'stripe_skip_html' => '' . esc_html__( 'Skip setting up Stripe', 'charitable' ) . '.',
'stripe_returned' => $stripe_returned,
'checklist_completed' => get_option( 'charitable_ss_complete', false ) ? true : false,
'test_mode' => ! empty( $_GET['test_mode'] ), // phpcs:ignore
);
$strings = apply_filters( 'charitable_setup_strings', $strings );
return (array) $strings;
}
/**
* Get the requested feature string for the install message about license activation.
*
* @since 1.8.4
*
* @return string
*/
public function get_requested_feature_string() {
$features = $this->features_to_install();
if ( empty( $features ) ) {
return '';
}
// create an array of names from feature_meta, with strong tags.
$features = array_map(
function ( $feature ) {
return '' . $this->feature_meta[ $feature ]['name'] . '';
},
$features
);
// make string with a comma and an 'and' before the last feature.
$features = implode( ', ', $features );
return $features;
}
/**
* Returns and/or create the single instance of this class.
*
* @since 1.8.4
*
* @return Charitable_Setup
*/
public static function get_instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
}
endif;