| 1 |
<?php |
| 2 |
/** |
| 3 |
* Admin Interface |
| 4 |
* |
| 5 |
* @package SureDonation |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace SureDonation\Inc\Admin; |
| 9 |
|
| 10 |
use SureDonation\Inc\Campaigns\Campaign_Cpt; |
| 11 |
use SureDonation\Inc\API\Onboarding_API; |
| 12 |
use SureDonation\Inc\Helper; |
| 13 |
use SureDonation\Inc\Onboarding; |
| 14 |
use SureDonation\Inc\Pdf\Pdf_Utils; |
| 15 |
use SureDonation\Inc\Payments\Payment_Helper; |
| 16 |
use SureDonation\Inc\Payments\PayPal\PayPal_Helper; |
| 17 |
use SureDonation\Inc\Payments\Stripe\Stripe_Helper; |
| 18 |
use SureDonation\Inc\Traits\Get_Instance; |
| 19 |
|
| 20 |
// Exit if accessed directly. |
| 21 |
if ( ! defined( 'ABSPATH' ) ) { |
| 22 |
exit; |
| 23 |
} |
| 24 |
|
| 25 |
/** |
| 26 |
* Admin class. |
| 27 |
* |
| 28 |
* @since 0.0.1 |
| 29 |
*/ |
| 30 |
class Admin { |
| 31 |
use Get_Instance; |
| 32 |
|
| 33 |
/** |
| 34 |
* Constructor. |
| 35 |
* |
| 36 |
* @since 0.0.1 |
| 37 |
*/ |
| 38 |
public function __construct() { |
| 39 |
add_action( 'admin_menu', [ $this, 'register_menu' ] ); |
| 40 |
add_action( 'admin_menu', [ $this, 'hide_forms_submenu' ], 999 ); |
| 41 |
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); |
| 42 |
add_action( 'admin_init', [ $this, 'redirect_cpt_listings' ] ); |
| 43 |
add_action( 'admin_init', [ $this, 'register_privacy_policy_content' ] ); |
| 44 |
add_action( 'admin_init', [ $this, 'register_react_notices' ], 5 ); |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Register the payment-gateway React admin notice. |
| 49 |
* |
| 50 |
* Bridges the payment/test-mode nudges into the React admin app via |
| 51 |
* Notice_Manager. Runs on admin_init (priority 5) so the notice is |
| 52 |
* registered before admin_enqueue_scripts localizes the data for React. |
| 53 |
* |
| 54 |
* Two mutually-exclusive states, mirroring the existing PHP/front-end |
| 55 |
* notice chain: |
| 56 |
* - No gateway connected -> prompt to configure a gateway. |
| 57 |
* - Connected, but site in test mode -> prompt to switch to live mode. |
| 58 |
* When a gateway is connected and the site is live, no notice shows. |
| 59 |
* |
| 60 |
* Capability-gated to manage_options; the data is only consumed on the |
| 61 |
* SureDonation admin pages, where the React app is enqueued. |
| 62 |
* |
| 63 |
* Hooked - admin_init (priority 5) |
| 64 |
* |
| 65 |
* @since 1.3.0 |
| 66 |
* @return void |
| 67 |
*/ |
| 68 |
public function register_react_notices() { |
| 69 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 70 |
return; |
| 71 |
} |
| 72 |
|
| 73 |
if ( ! Payment_Helper::is_any_gateway_connected() ) { |
| 74 |
Notice_Manager::register_notice( |
| 75 |
[ |
| 76 |
'id' => 'sd-configure-gateway', |
| 77 |
'variant' => 'error', |
| 78 |
// translators: the <a></a> tags wrap the inline link text and must be kept intact. |
| 79 |
'message' => __( 'Your forms cannot accept donations yet. <a>Connect Stripe or PayPal</a> and you can start accepting them today.', 'suredonation' ), |
| 80 |
'link' => [ |
| 81 |
// Deep-link to the gateway connect screen, matching the dashboard Quick Access entry. |
| 82 |
'url' => Payment_Helper::get_settings_url( 'stripe' ), |
| 83 |
], |
| 84 |
'event' => Analytics::TRACKED_EVENTS['configure_gateway'], |
| 85 |
'dismissible' => false, |
| 86 |
] |
| 87 |
); |
| 88 |
return; |
| 89 |
} |
| 90 |
|
| 91 |
if ( Stripe_Helper::is_stripe_connected() && ! Stripe_Helper::is_webhook_configured() ) { |
| 92 |
Notice_Manager::register_notice( |
| 93 |
[ |
| 94 |
'id' => 'sd-webhook-not-configured', |
| 95 |
'variant' => 'error', |
| 96 |
// translators: the <a></a> tags wrap the inline link text and must be kept intact. |
| 97 |
'message' => __( 'Webhooks keep SureDonation in sync with Stripe by automatically updating donation and subscription data. Please <a>configure</a> the webhook.', 'suredonation' ), |
| 98 |
'link' => [ |
| 99 |
'url' => Payment_Helper::get_settings_url( 'stripe' ), |
| 100 |
], |
| 101 |
'event' => Analytics::TRACKED_EVENTS['webhook'], |
| 102 |
'dismissible' => false, |
| 103 |
] |
| 104 |
); |
| 105 |
return; |
| 106 |
} |
| 107 |
|
| 108 |
if ( 'test' === Payment_Helper::get_payment_mode() ) { |
| 109 |
$settings_url = Payment_Helper::get_settings_url(); |
| 110 |
Notice_Manager::register_notice( |
| 111 |
[ |
| 112 |
'id' => 'sd-test-mode-react', |
| 113 |
'variant' => 'error', |
| 114 |
// translators: the <a></a> tags wrap the inline link text and must be kept intact. |
| 115 |
'message' => __( 'Supporters cannot donate while your site is in test mode. Anything they try now is a test and no money reaches you. <a>Switch to live mode</a> when you are ready to accept real donations.', 'suredonation' ), |
| 116 |
'link' => [ |
| 117 |
'url' => $settings_url, |
| 118 |
], |
| 119 |
'event' => Analytics::TRACKED_EVENTS['test_mode'], |
| 120 |
'dismissible' => false, |
| 121 |
] |
| 122 |
); |
| 123 |
} |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Register suggested privacy-policy text for the plugin's data transfers. |
| 128 |
* |
| 129 |
* Surfaces the Phone field's automatic country detection (which sends the |
| 130 |
* visitor's IP to ipapi.co) in the WordPress Privacy Policy Guide |
| 131 |
* (Settings → Privacy → Policy Guide) so site owners can disclose it in |
| 132 |
* their own policy. This only suggests text to admins — it publishes |
| 133 |
* nothing and is not shown to donors. |
| 134 |
* |
| 135 |
* @since 1.1.1 |
| 136 |
* @return void |
| 137 |
*/ |
| 138 |
public function register_privacy_policy_content() { |
| 139 |
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { |
| 140 |
return; |
| 141 |
} |
| 142 |
|
| 143 |
$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' ); |
| 144 |
|
| 145 |
wp_add_privacy_policy_content( 'SureDonation', wp_kses_post( wpautop( $content ) ) ); |
| 146 |
} |
| 147 |
|
| 148 |
/** |
| 149 |
* Redirect the default CPT list screens for suredonation_form and |
| 150 |
* suredonation_cmpgn to their corresponding plugin pages. |
| 151 |
* |
| 152 |
* For the form listing, attempt to resolve the originating campaign |
| 153 |
* from the referer (the "back" arrow on the form editor sends the |
| 154 |
* user here) and redirect to that campaign's single view. |
| 155 |
* |
| 156 |
* Campaign creation (post-new.php) is redirected too, so bookmarks and |
| 157 |
* third-party links to the raw editor land in the creation drawer rather |
| 158 |
* than producing a half-configured campaign. New forms keep using |
| 159 |
* post-new.php — the campaign screen links there with a campaign_id. |
| 160 |
* |
| 161 |
* @return void |
| 162 |
* @since 1.0.0 |
| 163 |
*/ |
| 164 |
public function redirect_cpt_listings() { |
| 165 |
global $pagenow; |
| 166 |
|
| 167 |
if ( 'edit.php' !== $pagenow && 'post-new.php' !== $pagenow ) { |
| 168 |
return; |
| 169 |
} |
| 170 |
|
| 171 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading post_type from query, no state mutation. |
| 172 |
$post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : ''; |
| 173 |
|
| 174 |
if ( 'post-new.php' === $pagenow ) { |
| 175 |
/* |
| 176 |
* Only for users who can actually open the destination: the admin app |
| 177 |
* requires manage_options, while the campaign post type is creatable |
| 178 |
* by any role with edit_posts. Redirecting those roles would trade the |
| 179 |
* editor for a permission error. |
| 180 |
*/ |
| 181 |
if ( Campaign_Cpt::POST_TYPE === $post_type && current_user_can( 'manage_options' ) ) { |
| 182 |
wp_safe_redirect( Campaign_Cpt::get_create_url() ); |
| 183 |
exit; |
| 184 |
} |
| 185 |
return; |
| 186 |
} |
| 187 |
|
| 188 |
if ( 'suredonation_form' === $post_type ) { |
| 189 |
$campaign_id = $this->resolve_campaign_id_from_referer(); |
| 190 |
$target = $campaign_id |
| 191 |
? admin_url( 'admin.php?page=suredonation#/campaigns/' . $campaign_id ) |
| 192 |
: admin_url( 'admin.php?page=suredonation#/campaigns' ); |
| 193 |
|
| 194 |
wp_safe_redirect( $target ); |
| 195 |
exit; |
| 196 |
} |
| 197 |
|
| 198 |
if ( Campaign_Cpt::POST_TYPE === $post_type ) { |
| 199 |
wp_safe_redirect( admin_url( 'admin.php?page=suredonation#/campaigns' ) ); |
| 200 |
exit; |
| 201 |
} |
| 202 |
} |
| 203 |
|
| 204 |
/** |
| 205 |
* Resolve the campaign ID from the referer when the form list page |
| 206 |
* is reached from the form editor's "back" button. |
| 207 |
* |
| 208 |
* @return int Campaign ID, or 0 when it cannot be determined. |
| 209 |
* @since 1.0.0 |
| 210 |
*/ |
| 211 |
private function resolve_campaign_id_from_referer() { |
| 212 |
$referer = wp_get_referer(); |
| 213 |
if ( ! $referer ) { |
| 214 |
return 0; |
| 215 |
} |
| 216 |
|
| 217 |
$parts = wp_parse_url( $referer ); |
| 218 |
if ( empty( $parts['query'] ) ) { |
| 219 |
return 0; |
| 220 |
} |
| 221 |
|
| 222 |
$query = []; |
| 223 |
parse_str( $parts['query'], $query ); |
| 224 |
|
| 225 |
if ( empty( $query['post'] ) || ! is_numeric( $query['post'] ) ) { |
| 226 |
return 0; |
| 227 |
} |
| 228 |
|
| 229 |
$form_id = absint( $query['post'] ); |
| 230 |
if ( 'suredonation_form' !== get_post_type( $form_id ) ) { |
| 231 |
return 0; |
| 232 |
} |
| 233 |
|
| 234 |
$campaign_id = get_post_meta( $form_id, '_suredonation_campaign_id', true ); |
| 235 |
if ( ! is_numeric( $campaign_id ) ) { |
| 236 |
return 0; |
| 237 |
} |
| 238 |
return absint( $campaign_id ); |
| 239 |
} |
| 240 |
|
| 241 |
/** |
| 242 |
* Enqueue admin scripts and styles. |
| 243 |
* |
| 244 |
* @param string $hook Current admin page hook. |
| 245 |
* @return void |
| 246 |
* @since 0.0.1 |
| 247 |
*/ |
| 248 |
public function enqueue_admin_scripts( $hook ) { |
| 249 |
// Only load on our plugin pages. |
| 250 |
if ( strpos( $hook, 'suredonation' ) === false ) { |
| 251 |
return; |
| 252 |
} |
| 253 |
|
| 254 |
// Onboarding gets its own lean bundle — bail before enqueuing the |
| 255 |
// main admin app on that page. |
| 256 |
if ( false !== strpos( $hook, Onboarding::PAGE_SLUG ) ) { |
| 257 |
$this->enqueue_onboarding_assets(); |
| 258 |
return; |
| 259 |
} |
| 260 |
|
| 261 |
$asset_file = SUREDONATION_DIR . 'assets/build/admin.asset.php'; |
| 262 |
|
| 263 |
if ( ! file_exists( $asset_file ) ) { |
| 264 |
return; |
| 265 |
} |
| 266 |
|
| 267 |
$asset = include $asset_file; |
| 268 |
|
| 269 |
// Enqueue WordPress editor for TinyMCE support. |
| 270 |
wp_enqueue_editor(); |
| 271 |
|
| 272 |
// Enqueue the media library so the campaign image picker can open it. |
| 273 |
wp_enqueue_media(); |
| 274 |
|
| 275 |
// Enqueue editor buttons style (needed for TinyMCE icons). |
| 276 |
wp_enqueue_style( 'editor-buttons' ); |
| 277 |
|
| 278 |
// Enqueue admin app script. |
| 279 |
wp_enqueue_script( |
| 280 |
'suredonation-admin', |
| 281 |
SUREDONATION_URL . 'assets/build/admin.js', |
| 282 |
$asset['dependencies'], |
| 283 |
$asset['version'], |
| 284 |
true |
| 285 |
); |
| 286 |
|
| 287 |
// Enqueue admin app styles. |
| 288 |
wp_enqueue_style( |
| 289 |
'suredonation-admin', |
| 290 |
SUREDONATION_URL . 'assets/build/admin.css', |
| 291 |
[], |
| 292 |
$asset['version'] |
| 293 |
); |
| 294 |
|
| 295 |
// The campaign drawer sits at a very high z-index. Lift the media library |
| 296 |
// above it, and keep the media modal above its OWN backdrop so the picker |
| 297 |
// is not dimmed by either overlay. |
| 298 |
wp_add_inline_style( |
| 299 |
'suredonation-admin', |
| 300 |
'.media-modal-backdrop{z-index:2147483646 !important;}.media-modal{z-index:2147483647 !important;}' |
| 301 |
); |
| 302 |
|
| 303 |
// Load JS translations for the admin app. |
| 304 |
wp_set_script_translations( 'suredonation-admin', 'suredonation' ); |
| 305 |
|
| 306 |
// Campaign guided tour resume point (empty when there is none). |
| 307 |
$campaign_tour_progress = get_user_meta( get_current_user_id(), Onboarding_API::TOUR_PROGRESS_META, true ); |
| 308 |
$campaign_tour_progress = is_string( $campaign_tour_progress ) ? $campaign_tour_progress : ''; |
| 309 |
|
| 310 |
// Campaign whose first-run tour the onboarding wizard armed (empty = none). |
| 311 |
$campaign_tour_pending = get_user_meta( get_current_user_id(), Onboarding_API::TOUR_PENDING_META, true ); |
| 312 |
$campaign_tour_pending = is_scalar( $campaign_tour_pending ) ? (string) $campaign_tour_pending : ''; |
| 313 |
|
| 314 |
// Localize script with plugin data. |
| 315 |
/** |
| 316 |
* Filters the bootstrap data localized to the SureDonation admin React app |
| 317 |
* (exposed as window.suredonation_admin). Lets features inject additional |
| 318 |
* data before it reaches React. |
| 319 |
* |
| 320 |
* @since 1.3.0 |
| 321 |
* @param array<string, mixed> $localized_data The admin app bootstrap data. |
| 322 |
*/ |
| 323 |
$localized_data = apply_filters( |
| 324 |
'suredonation_admin_app_data', |
| 325 |
[ |
| 326 |
'version' => SUREDONATION_VER, |
| 327 |
'apiUrl' => rest_url( 'suredonation/v1' ), |
| 328 |
'nonce' => wp_create_nonce( 'wp_rest' ), |
| 329 |
'docsURL' => 'https://suredonation.com/docs/', |
| 330 |
'plugin_url' => SUREDONATION_URL, |
| 331 |
'site_url' => site_url(), |
| 332 |
'admin_url' => admin_url(), |
| 333 |
'is_first_campaign_created' => $this->has_campaigns(), |
| 334 |
'is_onboarding_completed' => Onboarding::get_instance()->is_completed(), |
| 335 |
'campaign_tour_seen' => 'yes' === get_user_meta( get_current_user_id(), Onboarding_API::TOUR_SEEN_META, true ), |
| 336 |
'campaign_tour_progress' => $campaign_tour_progress, |
| 337 |
'campaign_tour_pending' => $campaign_tour_pending, |
| 338 |
'rotating_plugin_banner' => $this->get_rotating_plugin_banner(), |
| 339 |
'smart_tags' => Helper::get_smart_tags(), |
| 340 |
'is_pro_active' => defined( 'SUREDONATION_PRO_VER' ), |
| 341 |
'pro_plugin_version' => defined( 'SUREDONATION_PRO_VER' ) ? SUREDONATION_PRO_VER : '', |
| 342 |
'pro_plugin_name' => defined( 'SUREDONATION_PRO_PRODUCT' ) ? SUREDONATION_PRO_PRODUCT : 'SureDonation Pro', |
| 343 |
'is_license_active' => defined( 'SUREDONATION_PRO_VER' ) && class_exists( 'SureDonationPro\Inc\Licensing\Licensing' ) ? \SureDonationPro\Inc\Licensing\Licensing::is_license_active() : false, |
| 344 |
'pdf_library_exists' => Pdf_Utils::check_if_library_exists(), |
| 345 |
'php_version_compatible' => Pdf_Utils::is_php_compatible(), |
| 346 |
'pdf_nonce' => wp_create_nonce( 'suredonation_pdf_nonce' ), |
| 347 |
'ajax_url' => admin_url( 'admin-ajax.php' ), |
| 348 |
'plugin_installer_nonce' => wp_create_nonce( 'updates' ), |
| 349 |
'plugin_manager_nonce' => wp_create_nonce( 'suredonation_plugin_manager' ), |
| 350 |
'has_abilities_api' => class_exists( 'WP_Ability' ), |
| 351 |
'current_user_login' => wp_get_current_user()->user_login, |
| 352 |
'integrations' => [ |
| 353 |
'sure_triggers' => Helper::get_ottokit_integration(), |
| 354 |
], |
| 355 |
// Bootstrap settings data so the General currency list and the |
| 356 |
// PayPal connection state render synchronously (no post-mount |
| 357 |
// fetch flicker). PayPal data is the non-sensitive subset only. |
| 358 |
'payments' => [ |
| 359 |
'currencies' => Payment_Helper::get_currencies_list(), |
| 360 |
'paypal' => PayPal_Helper::get_connection_state(), |
| 361 |
'currency_sign_position' => Payment_Helper::get_currency_sign_position(), |
| 362 |
'is_any_gateway_connected' => Payment_Helper::is_any_gateway_connected(), |
| 363 |
'gateway_config_url' => Payment_Helper::get_settings_url( 'stripe' ), |
| 364 |
], |
| 365 |
] |
| 366 |
); |
| 367 |
|
| 368 |
wp_localize_script( 'suredonation-admin', 'suredonation_admin', $localized_data ); |
| 369 |
} |
| 370 |
|
| 371 |
/** |
| 372 |
* Register admin menu. |
| 373 |
* |
| 374 |
* @return void |
| 375 |
* @since 0.0.1 |
| 376 |
*/ |
| 377 |
public function register_menu() { |
| 378 |
$menu_slug = 'suredonation'; |
| 379 |
|
| 380 |
// SureDonation mark for the admin menu — single path with evenodd |
| 381 |
// fill so the inner heart cuts a hole that reveals the sidebar |
| 382 |
// background (mirrors the pattern used by SureForms). |
| 383 |
$menu_icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 3.64405C0 1.6315 1.62712 0 3.63425 0H20.2517C22.2588 0 23.886 1.6315 23.886 3.64405V20.3063C23.886 22.3188 22.2588 23.9503 20.2517 23.9503H3.63425C1.62712 23.9503 0 22.3188 0 20.3063V3.64405ZM12.6516 5.79655C14.4525 3.99075 17.3723 3.99075 19.1734 5.79655C20.9742 7.60234 20.9742 10.5301 19.1734 12.3359L17.5686 13.9451C17.2776 14.2368 16.8057 14.2368 16.5147 13.9451C16.2237 13.6533 16.2237 13.1802 16.5147 12.8884L18.1196 11.2793C19.3385 10.0571 19.3385 8.07537 18.1196 6.85312C16.9007 5.6309 14.9243 5.63092 13.7054 6.85312L11.3607 9.20415C11.0701 9.49552 11.0701 9.96791 11.3607 10.2593C11.6513 10.5506 12.1224 10.5506 12.413 10.2593L13.4659 9.20346C13.7569 8.91169 14.2288 8.91169 14.5198 9.20346C14.8107 9.49523 14.8107 9.96828 14.5198 10.26L13.4667 11.3159C12.5942 12.1909 11.1794 12.1909 10.3069 11.3159C9.43429 10.441 9.43429 9.02249 10.3069 8.14757L10.8879 7.56486L10.1781 6.85312C8.95914 5.63089 6.98277 5.63089 5.76382 6.85312C4.54486 8.07537 4.54488 10.0571 5.76382 11.2793L12.6985 18.2326C12.9895 18.5244 12.9895 18.9974 12.6985 19.2892C12.4075 19.581 11.9358 19.581 11.6448 19.2892L4.71008 12.3359C2.90915 10.5301 2.90913 7.60233 4.71008 5.79655C6.51102 3.99075 9.43091 3.99075 11.2318 5.79655L11.9417 6.50827L12.6516 5.79655ZM11.8605 14.9781C12.1515 14.6863 12.6233 14.6863 12.9143 14.9781L14.5397 16.6079C14.8307 16.8996 14.8307 17.3727 14.5397 17.6645C14.2487 17.9562 13.7769 17.9562 13.4859 17.6645L11.8605 16.0347C11.5695 15.7429 11.5695 15.2699 11.8605 14.9781ZM13.6482 13.1856C13.9392 12.8939 14.4109 12.8939 14.7019 13.1856L16.3273 14.8154C16.6183 15.1072 16.6183 15.5802 16.3273 15.872C16.0363 16.1638 15.5645 16.1638 15.2735 15.872L13.6482 14.2422C13.3572 13.9504 13.3572 13.4774 13.6482 13.1856Z" fill="#D1D5DB"/></svg>'; |
| 384 |
|
| 385 |
// Main menu page. |
| 386 |
add_menu_page( |
| 387 |
__( 'SureDonation', 'suredonation' ), |
| 388 |
__( 'SureDonation', 'suredonation' ), |
| 389 |
'manage_options', |
| 390 |
$menu_slug, |
| 391 |
[ $this, 'render_dashboard' ], |
| 392 |
'data:image/svg+xml;base64,' . base64_encode( $menu_icon_svg ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Building data URI for menu icon. |
| 393 |
25 |
| 394 |
); |
| 395 |
|
| 396 |
// Register submenus. |
| 397 |
$this->register_submenus( $menu_slug ); |
| 398 |
} |
| 399 |
|
| 400 |
/** |
| 401 |
* Render dashboard page. |
| 402 |
* |
| 403 |
* @return void |
| 404 |
* @since 0.0.1 |
| 405 |
*/ |
| 406 |
public function render_dashboard() { |
| 407 |
?> |
| 408 |
<div id="suredonation-admin-root"></div> |
| 409 |
<?php |
| 410 |
} |
| 411 |
|
| 412 |
/** |
| 413 |
* Hide the "All Forms" submenu item. |
| 414 |
* |
| 415 |
* WordPress automatically adds a submenu for the suredonation_form CPT. |
| 416 |
* We hide it since forms are managed through campaigns. |
| 417 |
* |
| 418 |
* @return void |
| 419 |
* @since 0.0.1 |
| 420 |
*/ |
| 421 |
public function hide_forms_submenu() { |
| 422 |
remove_submenu_page( 'suredonation', 'edit.php?post_type=suredonation_form' ); |
| 423 |
} |
| 424 |
|
| 425 |
/** |
| 426 |
* Check if any campaigns have been created. |
| 427 |
* |
| 428 |
* @return bool True if at least one campaign exists. |
| 429 |
* @since 0.0.1 |
| 430 |
*/ |
| 431 |
private function has_campaigns() { |
| 432 |
$campaigns = get_posts( |
| 433 |
[ |
| 434 |
'post_type' => 'suredonation_cmpgn', |
| 435 |
'posts_per_page' => 1, |
| 436 |
'post_status' => [ 'publish', 'draft', 'private' ], |
| 437 |
'fields' => 'ids', |
| 438 |
] |
| 439 |
); |
| 440 |
|
| 441 |
return ! empty( $campaigns ); |
| 442 |
} |
| 443 |
|
| 444 |
/** |
| 445 |
* Get rotating plugin banner data. |
| 446 |
* |
| 447 |
* @return array<string,mixed>|null Plugin banner data, or null when the plugin is already active. |
| 448 |
* @since 0.0.1 |
| 449 |
*/ |
| 450 |
private function get_rotating_plugin_banner() { |
| 451 |
// Match SureForms: never nudge a plugin that is already active, so the |
| 452 |
// dashboard card is hidden once SureRank is activated. |
| 453 |
$status = $this->get_plugin_status( 'surerank/surerank.php' ); |
| 454 |
if ( 'Activated' === $status ) { |
| 455 |
return null; |
| 456 |
} |
| 457 |
|
| 458 |
// Get SVG logo as base64. |
| 459 |
$logo_svg = file_get_contents( SUREDONATION_DIR . 'images/surerank.svg' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file read. |
| 460 |
$logo_data = $logo_svg ? 'data:image/svg+xml;base64,' . base64_encode( $logo_svg ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
| 461 |
|
| 462 |
// SureRank plugin data. |
| 463 |
return [ |
| 464 |
'slug' => 'surerank', |
| 465 |
'path' => 'surerank/surerank.php', |
| 466 |
'title' => 'SureRank', |
| 467 |
'singleLineDescription' => __( 'SEO Made Easy for Your Website', 'suredonation' ), |
| 468 |
'subtitle' => __( 'Beautiful pages, persuasive content, and custom code in seconds. The possibilities are endless!', 'suredonation' ), |
| 469 |
'logo' => $logo_data, |
| 470 |
'status' => $status, |
| 471 |
]; |
| 472 |
} |
| 473 |
|
| 474 |
/** |
| 475 |
* Get plugin installation status. |
| 476 |
* |
| 477 |
* @param string $plugin_file Plugin file path (e.g., 'plugin-folder/plugin-file.php'). |
| 478 |
* @return string Plugin status: 'Activated', 'Installed', or 'Install'. |
| 479 |
* @since 0.0.1 |
| 480 |
*/ |
| 481 |
private function get_plugin_status( $plugin_file ) { |
| 482 |
if ( ! function_exists( 'is_plugin_active' ) ) { |
| 483 |
include_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 484 |
} |
| 485 |
|
| 486 |
if ( is_plugin_active( $plugin_file ) ) { |
| 487 |
return 'Activated'; |
| 488 |
} |
| 489 |
|
| 490 |
$all_plugins = get_plugins(); |
| 491 |
if ( isset( $all_plugins[ $plugin_file ] ) ) { |
| 492 |
return 'Installed'; |
| 493 |
} |
| 494 |
|
| 495 |
return 'Install'; |
| 496 |
} |
| 497 |
|
| 498 |
/** |
| 499 |
* Register submenus. |
| 500 |
* |
| 501 |
* @param string $menu_slug Menu slug. |
| 502 |
* @return void |
| 503 |
* @since 0.0.1 |
| 504 |
*/ |
| 505 |
private function register_submenus( $menu_slug ) { |
| 506 |
$capability = 'manage_options'; |
| 507 |
|
| 508 |
$submenus = [ |
| 509 |
[ |
| 510 |
'id' => $menu_slug, |
| 511 |
'page_title' => __( 'Dashboard', 'suredonation' ), |
| 512 |
], |
| 513 |
[ |
| 514 |
'id' => 'suredonation#/campaigns', |
| 515 |
'page_title' => __( 'Campaigns', 'suredonation' ), |
| 516 |
], |
| 517 |
[ |
| 518 |
'id' => 'suredonation#/donations', |
| 519 |
'page_title' => __( 'Donations', 'suredonation' ), |
| 520 |
], |
| 521 |
[ |
| 522 |
'id' => 'suredonation#/donors', |
| 523 |
'page_title' => __( 'Donors', 'suredonation' ), |
| 524 |
], |
| 525 |
[ |
| 526 |
'id' => 'suredonation#/reports', |
| 527 |
'page_title' => __( 'Reports', 'suredonation' ), |
| 528 |
], |
| 529 |
[ |
| 530 |
'id' => 'suredonation#/settings', |
| 531 |
'page_title' => __( 'Settings', 'suredonation' ), |
| 532 |
], |
| 533 |
]; |
| 534 |
|
| 535 |
// Register the submenus. |
| 536 |
foreach ( $submenus as $submenu ) { |
| 537 |
add_submenu_page( |
| 538 |
$menu_slug, |
| 539 |
$submenu['page_title'], |
| 540 |
$submenu['page_title'], |
| 541 |
$capability, |
| 542 |
$submenu['id'], |
| 543 |
[ $this, 'render_dashboard' ] |
| 544 |
); |
| 545 |
} |
| 546 |
|
| 547 |
// Hidden onboarding page (no menu parent so it never shows in |
| 548 |
// the sidebar but is still routable via admin.php?page=...). |
| 549 |
add_submenu_page( |
| 550 |
'', |
| 551 |
__( 'SureDonation Setup', 'suredonation' ), |
| 552 |
__( 'SureDonation Setup', 'suredonation' ), |
| 553 |
$capability, |
| 554 |
Onboarding::PAGE_SLUG, |
| 555 |
[ $this, 'render_onboarding' ] |
| 556 |
); |
| 557 |
} |
| 558 |
|
| 559 |
/** |
| 560 |
* Render onboarding root container. |
| 561 |
* |
| 562 |
* @return void |
| 563 |
* @since 1.0.0 |
| 564 |
*/ |
| 565 |
public function render_onboarding() { |
| 566 |
?> |
| 567 |
<div id="suredonation-onboarding-root"></div> |
| 568 |
<?php |
| 569 |
} |
| 570 |
|
| 571 |
/** |
| 572 |
* Enqueue the dedicated onboarding bundle. |
| 573 |
* |
| 574 |
* Loaded only on the onboarding admin page so the main admin app |
| 575 |
* doesn't pay the cost on activation redirects. |
| 576 |
* |
| 577 |
* @return void |
| 578 |
* @since 1.0.0 |
| 579 |
*/ |
| 580 |
private function enqueue_onboarding_assets() { |
| 581 |
$asset_file = SUREDONATION_DIR . 'assets/build/admin/onboarding/index.asset.php'; |
| 582 |
|
| 583 |
if ( ! file_exists( $asset_file ) ) { |
| 584 |
return; |
| 585 |
} |
| 586 |
|
| 587 |
$asset = include $asset_file; |
| 588 |
|
| 589 |
wp_enqueue_script( |
| 590 |
'suredonation-onboarding', |
| 591 |
SUREDONATION_URL . 'assets/build/admin/onboarding/index.js', |
| 592 |
$asset['dependencies'], |
| 593 |
$asset['version'], |
| 594 |
true |
| 595 |
); |
| 596 |
|
| 597 |
wp_enqueue_style( |
| 598 |
'suredonation-onboarding', |
| 599 |
SUREDONATION_URL . 'assets/build/admin/onboarding/index.css', |
| 600 |
[], |
| 601 |
$asset['version'] |
| 602 |
); |
| 603 |
|
| 604 |
wp_set_script_translations( 'suredonation-onboarding', 'suredonation' ); |
| 605 |
|
| 606 |
$current_user = wp_get_current_user(); |
| 607 |
$is_pro = defined( 'SUREDONATION_PRO_VER' ); |
| 608 |
|
| 609 |
// Detect GiveWP rows once at enqueue time so the JS doesn't have to |
| 610 |
// spin a loader (and won't ever flash an error) just to decide whether |
| 611 |
// to render the optional migration step. |
| 612 |
$has_givewp_data = false; |
| 613 |
try { |
| 614 |
$has_givewp_data = \SureDonation\Inc\Import\Givewp\Source::get_instance()->has_givewp_data(); |
| 615 |
} catch ( \Throwable $e ) { |
| 616 |
$has_givewp_data = false; |
| 617 |
} |
| 618 |
|
| 619 |
$has_charitable_data = false; |
| 620 |
try { |
| 621 |
$has_charitable_data = \SureDonation\Inc\Import\Charitable\Source::get_instance()->has_charitable_data(); |
| 622 |
} catch ( \Throwable $e ) { |
| 623 |
$has_charitable_data = false; |
| 624 |
} |
| 625 |
|
| 626 |
wp_localize_script( |
| 627 |
'suredonation-onboarding', |
| 628 |
'suredonation_onboarding', |
| 629 |
[ |
| 630 |
'apiUrl' => rest_url( 'suredonation/v1' ), |
| 631 |
'nonce' => wp_create_nonce( 'wp_rest' ), |
| 632 |
'pluginUrl' => SUREDONATION_URL, |
| 633 |
'adminUrl' => admin_url(), |
| 634 |
'dashboardUrl' => admin_url( 'admin.php?page=suredonation' ), |
| 635 |
'paymentsUrl' => Payment_Helper::get_settings_url( 'stripe' ), |
| 636 |
'campaignsUrl' => admin_url( 'admin.php?page=suredonation#/campaigns' ), |
| 637 |
'docsUrl' => 'https://suredonation.com/docs/', |
| 638 |
'isProActive' => $is_pro, |
| 639 |
'hasGiveWPData' => $has_givewp_data, |
| 640 |
'hasCharitableData' => $has_charitable_data, |
| 641 |
'currentUser' => [ |
| 642 |
'firstName' => $current_user ? $current_user->first_name : '', |
| 643 |
'lastName' => $current_user ? $current_user->last_name : '', |
| 644 |
'email' => $current_user ? $current_user->user_email : '', |
| 645 |
], |
| 646 |
'privacyUrl' => 'https://suredonation.com/privacy-policy/', |
| 647 |
] |
| 648 |
); |
| 649 |
|
| 650 |
// Shim suredonation_admin on the onboarding page too — shared API |
| 651 |
// helpers (e.g. importApi.js) read apiUrl + nonce from that global, |
| 652 |
// so without this the migration session fails the REST cookie check. |
| 653 |
wp_localize_script( |
| 654 |
'suredonation-onboarding', |
| 655 |
'suredonation_admin', |
| 656 |
[ |
| 657 |
'apiUrl' => rest_url( 'suredonation/v1' ), |
| 658 |
'nonce' => wp_create_nonce( 'wp_rest' ), |
| 659 |
'is_pro_active' => $is_pro, |
| 660 |
] |
| 661 |
); |
| 662 |
} |
| 663 |
} |
| 664 |
|