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, ] ); } }