prompt to configure a gateway.
* - Connected, but site in test mode -> prompt to switch to live mode.
* When a gateway is connected and the site is live, no notice shows.
*
* Capability-gated to manage_options; the data is only consumed on the
* SureDonation admin pages, where the React app is enqueued.
*
* Hooked - admin_init (priority 5)
*
* @since 1.3.0
* @return void
*/
public function register_react_notices() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! Payment_Helper::is_any_gateway_connected() ) {
Notice_Manager::register_notice(
[
'id' => 'sd-configure-gateway',
'variant' => 'error',
// translators: the tags wrap the inline link text and must be kept intact.
'message' => __( 'Your forms cannot accept donations yet. Connect Stripe or PayPal and you can start accepting them today.', 'suredonation' ),
'link' => [
// Deep-link to the gateway connect screen, matching the dashboard Quick Access entry.
'url' => Payment_Helper::get_settings_url( 'stripe' ),
],
'event' => Analytics::TRACKED_EVENTS['configure_gateway'],
'dismissible' => false,
]
);
return;
}
if ( Stripe_Helper::is_stripe_connected() && ! Stripe_Helper::is_webhook_configured() ) {
Notice_Manager::register_notice(
[
'id' => 'sd-webhook-not-configured',
'variant' => 'error',
// translators: the tags wrap the inline link text and must be kept intact.
'message' => __( 'Webhooks keep SureDonation in sync with Stripe by automatically updating donation and subscription data. Please configure the webhook.', 'suredonation' ),
'link' => [
'url' => Payment_Helper::get_settings_url( 'stripe' ),
],
'event' => Analytics::TRACKED_EVENTS['webhook'],
'dismissible' => false,
]
);
return;
}
if ( 'test' === Payment_Helper::get_payment_mode() ) {
$settings_url = Payment_Helper::get_settings_url();
Notice_Manager::register_notice(
[
'id' => 'sd-test-mode-react',
'variant' => 'error',
// translators: the tags wrap the inline link text and must be kept intact.
'message' => __( 'Supporters cannot donate while your site is in test mode. Anything they try now is a test and no money reaches you. Switch to live mode when you are ready to accept real donations.', 'suredonation' ),
'link' => [
'url' => $settings_url,
],
'event' => Analytics::TRACKED_EVENTS['test_mode'],
'dismissible' => false,
]
);
}
}
/**
* Register suggested privacy-policy text for the plugin's data transfers.
*
* Surfaces the Phone field's automatic country detection (which sends the
* visitor's IP to ipapi.co) in the WordPress Privacy Policy Guide
* (Settings → Privacy → Policy Guide) so site owners can disclose it in
* their own policy. This only suggests text to admins — it publishes
* nothing and is not shown to donors.
*
* @since 1.1.1
* @return void
*/
public function register_privacy_policy_content() {
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
return;
}
$content = __( 'When a Phone field with automatic country detection is used, this site sends the visitor’s IP address to a third-party service (ipapi.co) to determine their country code. The IP address is not stored for this purpose; the resulting country code is cached temporarily. Automatic detection can be disabled per Phone field, or site-wide via the “suredonation_phone_geo_enabled” filter.', 'suredonation' );
wp_add_privacy_policy_content( 'SureDonation', wp_kses_post( wpautop( $content ) ) );
}
/**
* Redirect the default CPT list screens for suredonation_form and
* suredonation_cmpgn to their corresponding plugin pages.
*
* For the form listing, attempt to resolve the originating campaign
* from the referer (the "back" arrow on the form editor sends the
* user here) and redirect to that campaign's single view.
*
* Campaign creation (post-new.php) is redirected too, so bookmarks and
* third-party links to the raw editor land in the creation drawer rather
* than producing a half-configured campaign. New forms keep using
* post-new.php — the campaign screen links there with a campaign_id.
*
* @return void
* @since 1.0.0
*/
public function redirect_cpt_listings() {
global $pagenow;
if ( 'edit.php' !== $pagenow && 'post-new.php' !== $pagenow ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading post_type from query, no state mutation.
$post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : '';
if ( 'post-new.php' === $pagenow ) {
/*
* Only for users who can actually open the destination: the admin app
* requires manage_options, while the campaign post type is creatable
* by any role with edit_posts. Redirecting those roles would trade the
* editor for a permission error.
*/
if ( Campaign_Cpt::POST_TYPE === $post_type && current_user_can( 'manage_options' ) ) {
wp_safe_redirect( Campaign_Cpt::get_create_url() );
exit;
}
return;
}
if ( 'suredonation_form' === $post_type ) {
$campaign_id = $this->resolve_campaign_id_from_referer();
$target = $campaign_id
? admin_url( 'admin.php?page=suredonation#/campaigns/' . $campaign_id )
: admin_url( 'admin.php?page=suredonation#/campaigns' );
wp_safe_redirect( $target );
exit;
}
if ( Campaign_Cpt::POST_TYPE === $post_type ) {
wp_safe_redirect( admin_url( 'admin.php?page=suredonation#/campaigns' ) );
exit;
}
}
/**
* Resolve the campaign ID from the referer when the form list page
* is reached from the form editor's "back" button.
*
* @return int Campaign ID, or 0 when it cannot be determined.
* @since 1.0.0
*/
private function resolve_campaign_id_from_referer() {
$referer = wp_get_referer();
if ( ! $referer ) {
return 0;
}
$parts = wp_parse_url( $referer );
if ( empty( $parts['query'] ) ) {
return 0;
}
$query = [];
parse_str( $parts['query'], $query );
if ( empty( $query['post'] ) || ! is_numeric( $query['post'] ) ) {
return 0;
}
$form_id = absint( $query['post'] );
if ( 'suredonation_form' !== get_post_type( $form_id ) ) {
return 0;
}
$campaign_id = get_post_meta( $form_id, '_suredonation_campaign_id', true );
if ( ! is_numeric( $campaign_id ) ) {
return 0;
}
return absint( $campaign_id );
}
/**
* Enqueue admin scripts and styles.
*
* @param string $hook Current admin page hook.
* @return void
* @since 0.0.1
*/
public function enqueue_admin_scripts( $hook ) {
// Only load on our plugin pages.
if ( strpos( $hook, 'suredonation' ) === false ) {
return;
}
// Onboarding gets its own lean bundle — bail before enqueuing the
// main admin app on that page.
if ( false !== strpos( $hook, Onboarding::PAGE_SLUG ) ) {
$this->enqueue_onboarding_assets();
return;
}
$asset_file = SUREDONATION_DIR . 'assets/build/admin.asset.php';
if ( ! file_exists( $asset_file ) ) {
return;
}
$asset = include $asset_file;
// Enqueue WordPress editor for TinyMCE support.
wp_enqueue_editor();
// Enqueue the media library so the campaign image picker can open it.
wp_enqueue_media();
// Enqueue editor buttons style (needed for TinyMCE icons).
wp_enqueue_style( 'editor-buttons' );
// Enqueue admin app script.
wp_enqueue_script(
'suredonation-admin',
SUREDONATION_URL . 'assets/build/admin.js',
$asset['dependencies'],
$asset['version'],
true
);
// Enqueue admin app styles.
wp_enqueue_style(
'suredonation-admin',
SUREDONATION_URL . 'assets/build/admin.css',
[],
$asset['version']
);
// The campaign drawer sits at a very high z-index. Lift the media library
// above it, and keep the media modal above its OWN backdrop so the picker
// is not dimmed by either overlay.
wp_add_inline_style(
'suredonation-admin',
'.media-modal-backdrop{z-index:2147483646 !important;}.media-modal{z-index:2147483647 !important;}'
);
// Load JS translations for the admin app.
wp_set_script_translations( 'suredonation-admin', 'suredonation' );
// Campaign guided tour resume point (empty when there is none).
$campaign_tour_progress = get_user_meta( get_current_user_id(), Onboarding_API::TOUR_PROGRESS_META, true );
$campaign_tour_progress = is_string( $campaign_tour_progress ) ? $campaign_tour_progress : '';
// Campaign whose first-run tour the onboarding wizard armed (empty = none).
$campaign_tour_pending = get_user_meta( get_current_user_id(), Onboarding_API::TOUR_PENDING_META, true );
$campaign_tour_pending = is_scalar( $campaign_tour_pending ) ? (string) $campaign_tour_pending : '';
// Localize script with plugin data.
/**
* Filters the bootstrap data localized to the SureDonation admin React app
* (exposed as window.suredonation_admin). Lets features inject additional
* data before it reaches React.
*
* @since 1.3.0
* @param array $localized_data The admin app bootstrap data.
*/
$localized_data = apply_filters(
'suredonation_admin_app_data',
[
'version' => SUREDONATION_VER,
'apiUrl' => rest_url( 'suredonation/v1' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'docsURL' => 'https://suredonation.com/docs/',
'plugin_url' => SUREDONATION_URL,
'site_url' => site_url(),
'admin_url' => admin_url(),
'is_first_campaign_created' => $this->has_campaigns(),
'is_onboarding_completed' => Onboarding::get_instance()->is_completed(),
'campaign_tour_seen' => 'yes' === get_user_meta( get_current_user_id(), Onboarding_API::TOUR_SEEN_META, true ),
'campaign_tour_progress' => $campaign_tour_progress,
'campaign_tour_pending' => $campaign_tour_pending,
'rotating_plugin_banner' => $this->get_rotating_plugin_banner(),
'smart_tags' => Helper::get_smart_tags(),
'is_pro_active' => defined( 'SUREDONATION_PRO_VER' ),
'pro_plugin_version' => defined( 'SUREDONATION_PRO_VER' ) ? SUREDONATION_PRO_VER : '',
'pro_plugin_name' => defined( 'SUREDONATION_PRO_PRODUCT' ) ? SUREDONATION_PRO_PRODUCT : 'SureDonation Pro',
'is_license_active' => defined( 'SUREDONATION_PRO_VER' ) && class_exists( 'SureDonationPro\Inc\Licensing\Licensing' ) ? \SureDonationPro\Inc\Licensing\Licensing::is_license_active() : false,
'pdf_library_exists' => Pdf_Utils::check_if_library_exists(),
'php_version_compatible' => Pdf_Utils::is_php_compatible(),
'pdf_nonce' => wp_create_nonce( 'suredonation_pdf_nonce' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'plugin_installer_nonce' => wp_create_nonce( 'updates' ),
'plugin_manager_nonce' => wp_create_nonce( 'suredonation_plugin_manager' ),
'has_abilities_api' => class_exists( 'WP_Ability' ),
'current_user_login' => wp_get_current_user()->user_login,
'integrations' => [
'sure_triggers' => Helper::get_ottokit_integration(),
],
// Bootstrap settings data so the General currency list and the
// PayPal connection state render synchronously (no post-mount
// fetch flicker). PayPal data is the non-sensitive subset only.
'payments' => [
'currencies' => Payment_Helper::get_currencies_list(),
'paypal' => PayPal_Helper::get_connection_state(),
'currency_sign_position' => Payment_Helper::get_currency_sign_position(),
'is_any_gateway_connected' => Payment_Helper::is_any_gateway_connected(),
'gateway_config_url' => Payment_Helper::get_settings_url( 'stripe' ),
],
]
);
wp_localize_script( 'suredonation-admin', 'suredonation_admin', $localized_data );
}
/**
* Register admin menu.
*
* @return void
* @since 0.0.1
*/
public function register_menu() {
$menu_slug = 'suredonation';
// SureDonation mark for the admin menu — single path with evenodd
// fill so the inner heart cuts a hole that reveals the sidebar
// background (mirrors the pattern used by SureForms).
$menu_icon_svg = '';
// Main menu page.
add_menu_page(
__( 'SureDonation', 'suredonation' ),
__( 'SureDonation', 'suredonation' ),
'manage_options',
$menu_slug,
[ $this, 'render_dashboard' ],
'data:image/svg+xml;base64,' . base64_encode( $menu_icon_svg ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Building data URI for menu icon.
25
);
// Register submenus.
$this->register_submenus( $menu_slug );
}
/**
* Render dashboard page.
*
* @return void
* @since 0.0.1
*/
public function render_dashboard() {
?>
'suredonation_cmpgn',
'posts_per_page' => 1,
'post_status' => [ 'publish', 'draft', 'private' ],
'fields' => 'ids',
]
);
return ! empty( $campaigns );
}
/**
* Get rotating plugin banner data.
*
* @return array|null Plugin banner data, or null when the plugin is already active.
* @since 0.0.1
*/
private function get_rotating_plugin_banner() {
// Match SureForms: never nudge a plugin that is already active, so the
// dashboard card is hidden once SureRank is activated.
$status = $this->get_plugin_status( 'surerank/surerank.php' );
if ( 'Activated' === $status ) {
return null;
}
// Get SVG logo as base64.
$logo_svg = file_get_contents( SUREDONATION_DIR . 'images/surerank.svg' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file read.
$logo_data = $logo_svg ? 'data:image/svg+xml;base64,' . base64_encode( $logo_svg ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
// SureRank plugin data.
return [
'slug' => 'surerank',
'path' => 'surerank/surerank.php',
'title' => 'SureRank',
'singleLineDescription' => __( 'SEO Made Easy for Your Website', 'suredonation' ),
'subtitle' => __( 'Beautiful pages, persuasive content, and custom code in seconds. The possibilities are endless!', 'suredonation' ),
'logo' => $logo_data,
'status' => $status,
];
}
/**
* Get plugin installation status.
*
* @param string $plugin_file Plugin file path (e.g., 'plugin-folder/plugin-file.php').
* @return string Plugin status: 'Activated', 'Installed', or 'Install'.
* @since 0.0.1
*/
private function get_plugin_status( $plugin_file ) {
if ( ! function_exists( 'is_plugin_active' ) ) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_plugin_active( $plugin_file ) ) {
return 'Activated';
}
$all_plugins = get_plugins();
if ( isset( $all_plugins[ $plugin_file ] ) ) {
return 'Installed';
}
return 'Install';
}
/**
* Register submenus.
*
* @param string $menu_slug Menu slug.
* @return void
* @since 0.0.1
*/
private function register_submenus( $menu_slug ) {
$capability = 'manage_options';
$submenus = [
[
'id' => $menu_slug,
'page_title' => __( 'Dashboard', 'suredonation' ),
],
[
'id' => 'suredonation#/campaigns',
'page_title' => __( 'Campaigns', 'suredonation' ),
],
[
'id' => 'suredonation#/donations',
'page_title' => __( 'Donations', 'suredonation' ),
],
[
'id' => 'suredonation#/donors',
'page_title' => __( 'Donors', 'suredonation' ),
],
[
'id' => 'suredonation#/reports',
'page_title' => __( 'Reports', 'suredonation' ),
],
[
'id' => 'suredonation#/settings',
'page_title' => __( 'Settings', 'suredonation' ),
],
];
// Register the submenus.
foreach ( $submenus as $submenu ) {
add_submenu_page(
$menu_slug,
$submenu['page_title'],
$submenu['page_title'],
$capability,
$submenu['id'],
[ $this, 'render_dashboard' ]
);
}
// Hidden onboarding page (no menu parent so it never shows in
// the sidebar but is still routable via admin.php?page=...).
add_submenu_page(
'',
__( 'SureDonation Setup', 'suredonation' ),
__( 'SureDonation Setup', 'suredonation' ),
$capability,
Onboarding::PAGE_SLUG,
[ $this, 'render_onboarding' ]
);
}
/**
* Render onboarding root container.
*
* @return void
* @since 1.0.0
*/
public function render_onboarding() {
?>
has_givewp_data();
} catch ( \Throwable $e ) {
$has_givewp_data = false;
}
$has_charitable_data = false;
try {
$has_charitable_data = \SureDonation\Inc\Import\Charitable\Source::get_instance()->has_charitable_data();
} catch ( \Throwable $e ) {
$has_charitable_data = false;
}
wp_localize_script(
'suredonation-onboarding',
'suredonation_onboarding',
[
'apiUrl' => rest_url( 'suredonation/v1' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'pluginUrl' => SUREDONATION_URL,
'adminUrl' => admin_url(),
'dashboardUrl' => admin_url( 'admin.php?page=suredonation' ),
'paymentsUrl' => Payment_Helper::get_settings_url( 'stripe' ),
'campaignsUrl' => admin_url( 'admin.php?page=suredonation#/campaigns' ),
'docsUrl' => 'https://suredonation.com/docs/',
'isProActive' => $is_pro,
'hasGiveWPData' => $has_givewp_data,
'hasCharitableData' => $has_charitable_data,
'currentUser' => [
'firstName' => $current_user ? $current_user->first_name : '',
'lastName' => $current_user ? $current_user->last_name : '',
'email' => $current_user ? $current_user->user_email : '',
],
'privacyUrl' => 'https://suredonation.com/privacy-policy/',
]
);
// Shim suredonation_admin on the onboarding page too — shared API
// helpers (e.g. importApi.js) read apiUrl + nonce from that global,
// so without this the migration session fails the REST cookie check.
wp_localize_script(
'suredonation-onboarding',
'suredonation_admin',
[
'apiUrl' => rest_url( 'suredonation/v1' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'is_pro_active' => $is_pro,
]
);
}
}