payment-gateway-for-authorize-net-for-woocommerce
/
docs
/
deactivation-feedback-and-review-implementation-guide.md
deactivation-feedback-and-review-implementation-guide.md
804 lines
| 1 | # Deactivation Feedback + Review Request — Implementation Guide (DonateOcean Donation Suite) |
| 2 | |
| 3 | This guide explains how the **plugin deactivation (uninstall) feedback modal** and the |
| 4 | **"leave a review" admin notice** work in the *Payment Gateway for Authorize.Net for WooCommerce* |
| 5 | plugin, and gives a concrete, copy‑paste‑ready recipe to add **both features to the |
| 6 | [DonateOcean Donation Suite](https://github.com/easypaymentplugins/donateocean-donation-suite)** |
| 7 | plugin. |
| 8 | |
| 9 | Unlike the reference plugin (which is largely procedural), DonateOcean uses a **namespaced, OOP |
| 10 | architecture** with a PSR‑style autoloader and a `Bootstrap` that instantiates services. This guide |
| 11 | is written to match that architecture exactly, so the new code drops in cleanly. |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## 0. DonateOcean facts this guide is built on |
| 16 | |
| 17 | | Thing | Value in DonateOcean | |
| 18 | | ----------------------------- | -------------------------------------------------------------- | |
| 19 | | Main file | `donateocean-donation-suite.php` | |
| 20 | | Plugin display name | `DonateOcean – Donations via PayPal` | |
| 21 | | Plugin / WordPress.org slug | `donateocean-donation-suite` | |
| 22 | | Text domain | `donateocean-donation-suite` | |
| 23 | | Constants prefix | `DONADOSU_` (`DONADOSU_VERSION`, `DONADOSU_FILE`, `DONADOSU_PATH`, `DONADOSU_URL`) | |
| 24 | | Namespace | `DonationSuite\…` | |
| 25 | | Function / AJAX / option prefix | `donadosu` | |
| 26 | | Autoloader | `includes/class-autoloader.php` — `DonationSuite\` → `includes/`, sub‑namespace → lowercase dir, `StudlyCase` class → `class-kebab-case.php` | |
| 27 | | Service wiring | `DonationSuite\Core\Bootstrap::services()` calls `( new Class() )->register();` | |
| 28 | | Admin classes | `includes/admin/` → namespace `DonationSuite\Admin` | |
| 29 | | Settings page menu slug | `donadosu-settings` (submenu under `edit.php?post_type=donadosu_donation`) | |
| 30 | | Settings capability | `manage_options` | |
| 31 | | Admin asset gate | `isset($_GET['page']) && 'donadosu-settings' === $_GET['page']` | |
| 32 | | Existing AJAX nonce/object | nonce `donadosu_admin_actions`, localized object `donadosuAdmin` | |
| 33 | | **Activation timestamp** | **`donadosu_activated_at` already exists** — `time()` integer, set once by `DonationSuite\Core\Installer::activate()`, deleted in `uninstall.php` | |
| 34 | |
| 35 | > Because `donadosu_activated_at` already stores the activation time, the review notice **reuses it** |
| 36 | > instead of creating a new option — one fewer moving part than the reference plugin. |
| 37 | |
| 38 | --- |
| 39 | |
| 40 | ## 1. Files you will add / edit |
| 41 | |
| 42 | ``` |
| 43 | donateocean-donation-suite/ |
| 44 | ├── donateocean-donation-suite.php # EDIT: add DONADOSU_BASENAME constant |
| 45 | ├── uninstall.php # EDIT: delete the 2 review options |
| 46 | ├── includes/ |
| 47 | │ ├── core/ |
| 48 | │ │ └── class-bootstrap.php # EDIT: register the 2 new classes in services() |
| 49 | │ └── admin/ |
| 50 | │ ├── class-deactivation-feedback.php # NEW → DonationSuite\Admin\DeactivationFeedback |
| 51 | │ ├── class-review-notice.php # NEW → DonationSuite\Admin\ReviewNotice |
| 52 | │ └── views/ |
| 53 | │ └── deactivation-feedback-form.php # NEW (modal markup) |
| 54 | └── assets/ |
| 55 | ├── css/ |
| 56 | │ └── deactivation-feedback-modal.css # NEW (copy from reference, re-prefix) |
| 57 | ├── js/ |
| 58 | │ ├── deactivation-feedback-modal.js # NEW |
| 59 | │ └── review-ajax.js # NEW |
| 60 | └── fonts/ |
| 61 | ├── icomoon.eot # COPY verbatim from reference plugin |
| 62 | ├── icomoon.svg |
| 63 | ├── icomoon.ttf |
| 64 | └── icomoon.woff |
| 65 | ``` |
| 66 | |
| 67 | > File names matter — the autoloader maps `DonationSuite\Admin\DeactivationFeedback` → |
| 68 | > `includes/admin/class-deactivation-feedback.php` and `DonationSuite\Admin\ReviewNotice` → |
| 69 | > `includes/admin/class-review-notice.php`. Keep these exact, or the classes won't load. |
| 70 | |
| 71 | --- |
| 72 | |
| 73 | ## 2. Add the `DONADOSU_BASENAME` constant |
| 74 | |
| 75 | The deactivation modal needs the plugin basename (`folder/file.php`) to build the deactivate link. |
| 76 | DonateOcean doesn't define one yet, so add it next to the other constants in |
| 77 | `donateocean-donation-suite.php`: |
| 78 | |
| 79 | ```php |
| 80 | if ( ! defined( 'DONADOSU_BASENAME' ) ) { |
| 81 | define( 'DONADOSU_BASENAME', plugin_basename( DONADOSU_FILE ) ); |
| 82 | } |
| 83 | ``` |
| 84 | |
| 85 | --- |
| 86 | |
| 87 | # PART A — Deactivation (Uninstall) Feedback Modal |
| 88 | |
| 89 | When the admin clicks **Deactivate** on the Plugins screen, a modal intercepts the click and asks |
| 90 | *why*. The selected reason + optional free text + non‑PII environment info are sent to an Airtable |
| 91 | base, then the normal deactivation proceeds. **Deactivation is never blocked**, even if the network |
| 92 | call fails. |
| 93 | |
| 94 | ### Flow |
| 95 | |
| 96 | ``` |
| 97 | Plugins screen ──click "Deactivate"──► JS intercepts, opens modal |
| 98 | │ |
| 99 | user picks a reason (+ optional details) |
| 100 | │ |
| 101 | "Send & Deactivate" OR "I rather wouldn't say" |
| 102 | │ |
| 103 | JS $.post(ajaxurl, action=donadosu_send_deactivation, nonce, reason, reason_details) |
| 104 | │ |
| 105 | PHP: verify POST + nonce + activate_plugins ──► wp_remote_post() to Airtable |
| 106 | │ |
| 107 | PHP always returns success ──► JS redirects to the real deactivate URL |
| 108 | ``` |
| 109 | |
| 110 | ### A.1 — `includes/admin/class-deactivation-feedback.php` |
| 111 | |
| 112 | ```php |
| 113 | <?php |
| 114 | namespace DonationSuite\Admin; |
| 115 | |
| 116 | defined( 'ABSPATH' ) || exit; |
| 117 | |
| 118 | /** |
| 119 | * Deactivation feedback modal shown on the Plugins screen. |
| 120 | */ |
| 121 | class DeactivationFeedback { |
| 122 | |
| 123 | const NONCE = 'donadosu-deactivation'; |
| 124 | const ACTION = 'donadosu_send_deactivation'; |
| 125 | |
| 126 | /** Wire up hooks (called from Bootstrap::services()). */ |
| 127 | public function register(): void { |
| 128 | if ( ! is_admin() ) { |
| 129 | return; |
| 130 | } |
| 131 | add_action( 'admin_footer', array( $this, 'render_modal' ) ); |
| 132 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); |
| 133 | add_action( 'wp_ajax_' . self::ACTION, array( $this, 'handle_request' ) ); |
| 134 | } |
| 135 | |
| 136 | /** Print the modal markup in the footer of the Plugins screen only. */ |
| 137 | public function render_modal(): void { |
| 138 | global $pagenow; |
| 139 | if ( 'plugins.php' !== $pagenow ) { |
| 140 | return; |
| 141 | } |
| 142 | include_once DONADOSU_PATH . 'includes/admin/views/deactivation-feedback-form.php'; |
| 143 | } |
| 144 | |
| 145 | /** Enqueue modal CSS/JS on the Plugins screen only, and hand a nonce to JS. */ |
| 146 | public function enqueue_assets(): void { |
| 147 | global $pagenow; |
| 148 | if ( 'plugins.php' !== $pagenow ) { |
| 149 | return; |
| 150 | } |
| 151 | wp_enqueue_script( 'jquery-blockui' ); // loading overlay used by the modal |
| 152 | wp_enqueue_style( |
| 153 | 'donadosu-deactivation-feedback', |
| 154 | DONADOSU_URL . 'assets/css/deactivation-feedback-modal.css', |
| 155 | array(), |
| 156 | DONADOSU_VERSION |
| 157 | ); |
| 158 | wp_enqueue_script( |
| 159 | 'donadosu-deactivation-feedback', |
| 160 | DONADOSU_URL . 'assets/js/deactivation-feedback-modal.js', |
| 161 | array( 'jquery' ), |
| 162 | DONADOSU_VERSION, |
| 163 | true |
| 164 | ); |
| 165 | wp_localize_script( |
| 166 | 'donadosu-deactivation-feedback', |
| 167 | 'donadosuFeedback', |
| 168 | array( 'nonce' => wp_create_nonce( self::NONCE ) ) |
| 169 | ); |
| 170 | } |
| 171 | |
| 172 | /** AJAX handler: validate, forward to Airtable, never block deactivation. */ |
| 173 | public function handle_request(): void { |
| 174 | $method = isset( $_SERVER['REQUEST_METHOD'] ) |
| 175 | ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; |
| 176 | if ( 'POST' !== strtoupper( $method ) ) { |
| 177 | wp_send_json_error( array( 'message' => __( 'Invalid request method.', 'donateocean-donation-suite' ) ), 405 ); |
| 178 | } |
| 179 | |
| 180 | check_ajax_referer( self::NONCE, 'nonce' ); // CSRF protection |
| 181 | |
| 182 | if ( ! current_user_can( 'activate_plugins' ) ) { |
| 183 | wp_send_json_error( array( 'message' => 'Unauthorized.' ), 403 ); |
| 184 | } |
| 185 | |
| 186 | $reason = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : ''; |
| 187 | $reason_details = isset( $_POST['reason_details'] ) ? sanitize_text_field( wp_unslash( $_POST['reason_details'] ) ) : ''; |
| 188 | |
| 189 | // Use your OWN Airtable base/token, or reuse the team base and just change 'plugin'. |
| 190 | $url = 'https://api.airtable.com/v0/YOUR_AIRTABLE_BASE_ID/Sheet1'; |
| 191 | $api_key = 'YOUR_AIRTABLE_PAT'; // see Security note — prefer a constant/option over hardcoding |
| 192 | |
| 193 | $data = array( |
| 194 | 'reason' => $reason . ( $reason_details ? ' : ' . $reason_details : '' ), |
| 195 | 'plugin' => 'DonateOcean', // <-- identifies THIS plugin in a shared base |
| 196 | 'php_version' => phpversion(), |
| 197 | 'wp_version' => get_bloginfo( 'version' ), |
| 198 | 'wc_version' => defined( 'WC_VERSION' ) ? WC_VERSION : '', |
| 199 | 'locale' => get_locale(), |
| 200 | 'theme' => wp_get_theme()->get( 'Name' ), |
| 201 | 'theme_version' => wp_get_theme()->get( 'Version' ), |
| 202 | 'multisite' => is_multisite() ? 'Yes' : 'No', |
| 203 | 'plugin_version' => defined( 'DONADOSU_VERSION' ) ? DONADOSU_VERSION : '', |
| 204 | ); |
| 205 | |
| 206 | $args = array( |
| 207 | 'headers' => array( |
| 208 | 'Authorization' => 'Bearer ' . $api_key, |
| 209 | 'Content-Type' => 'application/json', |
| 210 | ), |
| 211 | 'body' => wp_json_encode( array( |
| 212 | 'records' => array( |
| 213 | array( |
| 214 | 'fields' => array( |
| 215 | 'reason' => wp_json_encode( $data ), |
| 216 | 'date' => current_time( 'mysql' ), |
| 217 | ), |
| 218 | ), |
| 219 | ), |
| 220 | ) ), |
| 221 | 'method' => 'POST', |
| 222 | 'timeout' => 10, |
| 223 | ); |
| 224 | |
| 225 | // Fire-and-forget: the result never blocks the deactivation UX. |
| 226 | wp_remote_post( $url, $args ); |
| 227 | wp_send_json_success( array( 'message' => 'Feedback received.' ) ); |
| 228 | } |
| 229 | } |
| 230 | ``` |
| 231 | |
| 232 | ### A.2 — `includes/admin/views/deactivation-feedback-form.php` |
| 233 | |
| 234 | ```php |
| 235 | <?php |
| 236 | defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); |
| 237 | $donadosu_deactivation_url = wp_nonce_url( |
| 238 | 'plugins.php?action=deactivate&plugin=' . rawurlencode( DONADOSU_BASENAME ), |
| 239 | 'deactivate-plugin_' . DONADOSU_BASENAME |
| 240 | ); |
| 241 | ?> |
| 242 | <div class="donadosu-deactivation-Modal"> |
| 243 | <div class="donadosu-deactivation-Modal-header"> |
| 244 | <div> |
| 245 | <button class="donadosu-deactivation-Modal-return deactivation-icon-chevron-left"><?php esc_html_e( 'Return', 'donateocean-donation-suite' ); ?></button> |
| 246 | <h2><?php esc_html_e( 'We’re sorry to see you go! 💔', 'donateocean-donation-suite' ); ?></h2> |
| 247 | </div> |
| 248 | <button class="donadosu-deactivation-Modal-close deactivation-icon-close"><?php esc_html_e( 'Close', 'donateocean-donation-suite' ); ?></button> |
| 249 | </div> |
| 250 | <div class="donadosu-deactivation-Modal-content"> |
| 251 | <div class="donadosu-deactivation-Modal-question deactivation-isOpen"> |
| 252 | <p><?php esc_html_e( 'Can you please tell us why you’re deactivating the plugin? Your feedback helps us make it better.', 'donateocean-donation-suite' ); ?></p> |
| 253 | <ul> |
| 254 | <li> |
| 255 | <input type="radio" name="reason" id="reason-temporary" value="Temporary Deactivation"> |
| 256 | <label for="reason-temporary"><?php esc_html_e( 'Temporary deactivation (troubleshooting)', 'donateocean-donation-suite' ); ?></label> |
| 257 | </li> |
| 258 | <li> |
| 259 | <input type="radio" name="reason" id="reason-broke" value="Broken Layout"> |
| 260 | <label for="reason-broke"><?php esc_html_e( 'Compatibility issue', 'donateocean-donation-suite' ); ?></label> |
| 261 | <div class="donadosu-deactivation-Modal-fieldHidden"> |
| 262 | <textarea placeholder="<?php esc_attr_e( 'Please describe what part of the layout or functionality was affected.', 'donateocean-donation-suite' ); ?>"></textarea> |
| 263 | </div> |
| 264 | </li> |
| 265 | <li> |
| 266 | <input type="radio" name="reason" id="reason-complicated" value="Complicated"> |
| 267 | <label for="reason-complicated"><?php esc_html_e( 'Difficult to set up', 'donateocean-donation-suite' ); ?></label> |
| 268 | <div class="donadosu-deactivation-Modal-fieldHidden"> |
| 269 | <textarea placeholder="<?php esc_attr_e( 'What part of the setup was confusing or unclear?', 'donateocean-donation-suite' ); ?>"></textarea> |
| 270 | </div> |
| 271 | </li> |
| 272 | <li> |
| 273 | <input type="radio" name="reason" id="not-provided" value="features not provided"> |
| 274 | <label for="not-provided"><?php esc_html_e( 'Missing features', 'donateocean-donation-suite' ); ?></label> |
| 275 | <div class="donadosu-deactivation-Modal-fieldHidden"> |
| 276 | <textarea placeholder="<?php esc_attr_e( 'Which features were you looking for?', 'donateocean-donation-suite' ); ?>"></textarea> |
| 277 | </div> |
| 278 | </li> |
| 279 | <li> |
| 280 | <input type="radio" name="reason" id="reason-other" value="Other"> |
| 281 | <label for="reason-other"><?php esc_html_e( 'Other', 'donateocean-donation-suite' ); ?></label> |
| 282 | <div class="donadosu-deactivation-Modal-fieldHidden"> |
| 283 | <textarea placeholder="<?php esc_attr_e( 'Please share why you’re deactivating DonateOcean so we can make improvements.', 'donateocean-donation-suite' ); ?>"></textarea> |
| 284 | </div> |
| 285 | </li> |
| 286 | </ul> |
| 287 | <input id="deactivation-reason" type="hidden" value=""> |
| 288 | <input id="deactivation-details" type="hidden" value=""> |
| 289 | </div> |
| 290 | <p style="margin-top: 20px;"> |
| 291 | <?php esc_html_e( 'Your privacy is important to us. No personal data is collected with this form—just your valuable feedback and basic system information (such as WordPress and plugin versions) to help us improve our plugin.', 'donateocean-donation-suite' ); ?> |
| 292 | </p> |
| 293 | </div> |
| 294 | |
| 295 | <div class="donadosu-deactivation-Modal-footer"> |
| 296 | <a href="https://wordpress.org/support/plugin/donateocean-donation-suite" class="button button-primary" target="_blank" title="<?php esc_attr_e( 'Visit our support page for assistance', 'donateocean-donation-suite' ); ?>"><?php esc_html_e( 'Get Support', 'donateocean-donation-suite' ); ?></a> |
| 297 | <div> |
| 298 | <a href="<?php echo esc_attr( $donadosu_deactivation_url ); ?>" class="button button-primary deactivation-isDisabled" disabled id="donadosu-send-deactivation"><?php esc_html_e( 'Send & Deactivate', 'donateocean-donation-suite' ); ?></a> |
| 299 | </div> |
| 300 | <a id="donadosu-deactivation-no-reason" href="<?php echo esc_attr( $donadosu_deactivation_url ); ?>" class=""><?php esc_html_e( 'I rather wouldn\'t say', 'donateocean-donation-suite' ); ?></a> |
| 301 | </div> |
| 302 | </div> |
| 303 | <div class="donadosu-deactivation-Modal-overlay"></div> |
| 304 | ``` |
| 305 | |
| 306 | ### A.3 — `assets/js/deactivation-feedback-modal.js` |
| 307 | |
| 308 | > The opener selector's `data-slug` is **`donateocean-donation-suite`** (the plugin folder slug). If |
| 309 | > the installed folder is named differently, match that instead, or the modal won't open. |
| 310 | |
| 311 | ```js |
| 312 | (function ($) { |
| 313 | 'use strict'; |
| 314 | |
| 315 | $(document).ready(function () { |
| 316 | const $modal = $(".donadosu-deactivation-Modal"); |
| 317 | if ($modal.length) { |
| 318 | new DonadosuDeactivationModal($modal); |
| 319 | } |
| 320 | |
| 321 | // "I rather wouldn't say" — send a generic reason, then deactivate. |
| 322 | $("#donadosu-deactivation-no-reason").on("click", function (e) { |
| 323 | e.preventDefault(); |
| 324 | $('.donadosu-deactivation-Modal').block({ message: null, overlayCSS: { background: '#fff', opacity: 0.6 } }); |
| 325 | const url = $(this).attr("href"); |
| 326 | $.post(ajaxurl, { |
| 327 | action: 'donadosu_send_deactivation', |
| 328 | nonce: (window.donadosuFeedback && donadosuFeedback.nonce) ? donadosuFeedback.nonce : '', |
| 329 | reason: 'reason-other', |
| 330 | reason_details: 'other' |
| 331 | }).always(function () { window.location.replace(url); }); |
| 332 | }); |
| 333 | |
| 334 | // "Send & Deactivate" — send the selected reason + details, then deactivate. |
| 335 | $("#donadosu-send-deactivation").on("click", function (e) { |
| 336 | e.preventDefault(); |
| 337 | $('.donadosu-deactivation-Modal').block({ message: null, overlayCSS: { background: '#fff', opacity: 0.6 } }); |
| 338 | |
| 339 | const $button = $('#donadosu-send-deactivation'); |
| 340 | const selected = $("input[name='reason']:checked"); |
| 341 | const reason = selected.val(); |
| 342 | const reasonDetails = selected.siblings('.donadosu-deactivation-Modal-fieldHidden').find('textarea').val(); |
| 343 | |
| 344 | if (!reason) { alert("Please select a reason before deactivating."); return; } |
| 345 | $button.prop('disabled', true).css({ cursor: 'not-allowed', opacity: '0.6' }); |
| 346 | |
| 347 | $.post(ajaxurl, { |
| 348 | action: 'donadosu_send_deactivation', |
| 349 | nonce: (window.donadosuFeedback && donadosuFeedback.nonce) ? donadosuFeedback.nonce : '', |
| 350 | reason: reason, |
| 351 | reason_details: reasonDetails || '' |
| 352 | }).always(function () { window.location.replace($button.attr("href")); }); |
| 353 | }); |
| 354 | }); |
| 355 | |
| 356 | class DonadosuDeactivationModal { |
| 357 | constructor($elem) { |
| 358 | this.$elem = $elem; |
| 359 | this.$overlay = $('.donadosu-deactivation-Modal-overlay', $elem); |
| 360 | this.$radio = $('input[name=reason]', $elem); |
| 361 | this.$closer = $('.donadosu-deactivation-Modal-close, .donadosu-deactivation-Modal-cancel', $elem); |
| 362 | this.$returnBtn = $('.donadosu-deactivation-Modal-return', $elem); |
| 363 | this.$opener = $('.plugins [data-slug="donateocean-donation-suite"] .deactivate'); |
| 364 | this.$question = $('.donadosu-deactivation-Modal-question', $elem); |
| 365 | this.$button = $('.button-primary', $elem); |
| 366 | this.$title = $('.donadosu-deactivation-Modal-header h2', $elem); |
| 367 | this.$textFields = $('input[type=text], textarea', $elem); |
| 368 | this.$hiddenReason = $('#deactivation-reason', $elem); |
| 369 | this.$hiddenDetails = $('#deactivation-details', $elem); |
| 370 | this.titleText = this.$title.text(); |
| 371 | this.bindEvents(); |
| 372 | } |
| 373 | |
| 374 | bindEvents() { |
| 375 | this.$opener.on("click", (e) => { e.preventDefault(); this.open(); }); |
| 376 | this.$closer.on("click", (e) => { e.preventDefault(); this.close(); }); |
| 377 | this.$elem.on("keyup", (event) => { if (event.keyCode === 27) { this.close(); } }); |
| 378 | this.$returnBtn.on("click", (e) => { e.preventDefault(); this.returnToQuestion(); }); |
| 379 | this.$radio.on("change", (e) => { this.change($(e.currentTarget)); }); |
| 380 | this.$textFields.on("keyup", (e) => { |
| 381 | const value = $(e.currentTarget).val(); |
| 382 | this.$hiddenDetails.val(value); |
| 383 | if (value !== '') { |
| 384 | this.$button.removeClass('deactivation-isDisabled').removeAttr("disabled"); |
| 385 | } else { |
| 386 | this.$button.addClass('deactivation-isDisabled').attr("disabled", true); |
| 387 | } |
| 388 | }); |
| 389 | } |
| 390 | |
| 391 | change($elem) { |
| 392 | this.$hiddenReason.val($elem.val()); |
| 393 | this.$hiddenDetails.val(''); |
| 394 | this.$textFields.val(''); |
| 395 | $('.donadosu-deactivation-Modal-fieldHidden', this.$elem).removeClass('deactivation-isOpen'); |
| 396 | const $field = $elem.siblings('.donadosu-deactivation-Modal-fieldHidden'); |
| 397 | if ($field.length) { |
| 398 | $field.addClass('deactivation-isOpen'); |
| 399 | $field.find('textarea').focus(); |
| 400 | this.$button.addClass('deactivation-isDisabled').attr("disabled", true); |
| 401 | } else { |
| 402 | this.$button.removeClass('deactivation-isDisabled').removeAttr("disabled"); |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | returnToQuestion() { |
| 407 | $('.donadosu-deactivation-Modal-fieldHidden', this.$elem).removeClass('deactivation-isOpen'); |
| 408 | this.$question.addClass('deactivation-isOpen'); |
| 409 | this.$returnBtn.removeClass('deactivation-isOpen'); |
| 410 | this.$title.text(this.titleText); |
| 411 | this.$hiddenReason.val(''); |
| 412 | this.$hiddenDetails.val(''); |
| 413 | this.$radio.prop('checked', false); |
| 414 | this.$button.addClass('deactivation-isDisabled').attr("disabled", true); |
| 415 | } |
| 416 | |
| 417 | open() { |
| 418 | this.$elem.show(); |
| 419 | $('.donadosu-deactivation-Modal-overlay').show(); |
| 420 | } |
| 421 | |
| 422 | close() { |
| 423 | this.returnToQuestion(); |
| 424 | this.$elem.hide(); |
| 425 | $('.donadosu-deactivation-Modal-overlay').hide(); |
| 426 | } |
| 427 | } |
| 428 | })(jQuery); |
| 429 | ``` |
| 430 | |
| 431 | ### A.4 — `assets/css/deactivation-feedback-modal.css` |
| 432 | |
| 433 | Copy `feedback/css/deactivation-feedback-modal.css` from the reference plugin **verbatim**, then |
| 434 | find‑and‑replace the class prefix `easyauthnet-deactivation-Modal` → `donadosu-deactivation-Modal`. |
| 435 | Leave the `@font-face` block and the `deactivation-icon-*` classes untouched. The font URLs are |
| 436 | relative (`../fonts/icomoon.*`); with the CSS at `assets/css/` and the fonts at `assets/fonts/`, they |
| 437 | resolve correctly. (If you prefer not to re‑prefix, you may keep the original `easyauthnet-*` class |
| 438 | names as long as the markup and JS use the exact same names — but re‑prefixing keeps things tidy and |
| 439 | namespaced.) |
| 440 | |
| 441 | --- |
| 442 | |
| 443 | # PART B — "Leave a Review" Admin Notice |
| 444 | |
| 445 | A dismissible success notice on the **Donation Suite → Settings** screen (`page=donadosu-settings`), |
| 446 | shown only after the plugin has been active for **at least 1 day**, with **Remind me later** (7‑day |
| 447 | snooze) and **permanent dismissal**. |
| 448 | |
| 449 | ### Flow |
| 450 | |
| 451 | ``` |
| 452 | admin_notices on the settings page (page=donadosu-settings) |
| 453 | │ |
| 454 | ReviewNotice::render(): |
| 455 | - render-once guard + screen check + manage_options check |
| 456 | - read donadosu_activated_at (existing activation timestamp) |
| 457 | - skip if hide='never' OR active < 1 day OR now < next_show |
| 458 | - else print notice (Write Review / Done! / Remind me later / Hide) |
| 459 | │ |
| 460 | click ──► assets/js/review-ajax.js |
| 461 | │ |
| 462 | $.post(action=donadosu_handle_review_action, review_action, nonce) |
| 463 | │ |
| 464 | ReviewNotice::handle_action(): |
| 465 | - 'later' → next_show = now + 7 days ; hide = 'later' |
| 466 | - 'never'/'reviewed' → hide = 'never' |
| 467 | ``` |
| 468 | |
| 469 | ### Options used |
| 470 | |
| 471 | | Option | Values | Purpose | |
| 472 | | ------------------------------- | ---------------------- | ---------------------------------------- | |
| 473 | | `donadosu_activated_at` | unix timestamp | **Existing** — set by the Installer. | |
| 474 | | `donadosu_review_notice_hide` | `''`, `later`, `never` | Dismissal state. | |
| 475 | | `donadosu_review_next_show` | unix timestamp | Earliest time to show again (snooze). | |
| 476 | |
| 477 | ### B.1 — `includes/admin/class-review-notice.php` |
| 478 | |
| 479 | ```php |
| 480 | <?php |
| 481 | namespace DonationSuite\Admin; |
| 482 | |
| 483 | defined( 'ABSPATH' ) || exit; |
| 484 | |
| 485 | /** |
| 486 | * "Leave a review" admin notice shown on the Donation Suite settings page. |
| 487 | */ |
| 488 | class ReviewNotice { |
| 489 | |
| 490 | const HIDE_OPTION = 'donadosu_review_notice_hide'; |
| 491 | const NEXT_OPTION = 'donadosu_review_next_show'; |
| 492 | const NONCE = 'donadosu_review_nonce'; |
| 493 | const ACTION = 'donadosu_handle_review_action'; |
| 494 | const REVIEW_URL = 'https://wordpress.org/support/plugin/donateocean-donation-suite/reviews/#new-post'; |
| 495 | const PAGE_SLUG = 'donadosu-settings'; |
| 496 | |
| 497 | private static $rendered = false; |
| 498 | |
| 499 | /** Wire up hooks (called from Bootstrap::services()). */ |
| 500 | public function register(): void { |
| 501 | if ( ! is_admin() ) { |
| 502 | return; |
| 503 | } |
| 504 | add_action( 'admin_notices', array( $this, 'render' ) ); |
| 505 | add_action( 'wp_ajax_' . self::ACTION, array( $this, 'handle_action' ) ); |
| 506 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); |
| 507 | add_filter( 'safe_style_css', array( $this, 'allowed_css' ) ); |
| 508 | } |
| 509 | |
| 510 | private function is_settings_screen(): bool { |
| 511 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing |
| 512 | return isset( $_GET['page'] ) && self::PAGE_SLUG === sanitize_key( wp_unslash( $_GET['page'] ) ); |
| 513 | } |
| 514 | |
| 515 | public function render(): void { |
| 516 | if ( self::$rendered || ! $this->is_settings_screen() || ! current_user_can( 'manage_options' ) ) { |
| 517 | return; |
| 518 | } |
| 519 | self::$rendered = true; |
| 520 | |
| 521 | // Reuse the activation timestamp the Installer already stores. |
| 522 | $activation_time = (int) get_option( 'donadosu_activated_at' ); |
| 523 | if ( empty( $activation_time ) ) { |
| 524 | $activation_time = time(); |
| 525 | } |
| 526 | |
| 527 | $hide_state = get_option( self::HIDE_OPTION, '' ); // '', 'later', 'never' |
| 528 | $next_show = (int) get_option( self::NEXT_OPTION, time() ); |
| 529 | $since_active = time() - $activation_time; |
| 530 | |
| 531 | if ( 'never' === $hide_state || $since_active < DAY_IN_SECONDS || time() < $next_show ) { |
| 532 | return; |
| 533 | } |
| 534 | |
| 535 | $plugin_name = 'DonateOcean – Donations via PayPal'; |
| 536 | $review_url = self::REVIEW_URL; |
| 537 | |
| 538 | $html = '<div class="notice notice-success donadosu-review-notice">'; |
| 539 | $html .= '<p style="font-size:100%"></p>'; |
| 540 | $html .= '<h2 style="margin:0" class="title">' . |
| 541 | sprintf( |
| 542 | /* translators: %s: plugin name */ |
| 543 | esc_html__( 'Thank you for using %s 💕', 'donateocean-donation-suite' ), |
| 544 | '<b>' . esc_html( $plugin_name ) . '</b>' |
| 545 | ) . '</h2>'; |
| 546 | |
| 547 | $html .= '<p>' . |
| 548 | sprintf( |
| 549 | wp_kses( |
| 550 | /* translators: %1$s: URL to the plugin review page */ |
| 551 | __( 'If you have a moment, we’d love it if you could leave us a <b><a target="_blank" href="%1$s">quick review</a>.</b> It motivates us and helps us keep improving. 💫 <br>Have feature ideas? Include them in your review — your feedback shapes our roadmap, and we love turning your ideas into reality.', 'donateocean-donation-suite' ), |
| 552 | array( 'b' => array(), 'a' => array( 'href' => array(), 'target' => array() ), 'br' => array() ) |
| 553 | ), |
| 554 | esc_url( $review_url ) |
| 555 | ) . '</p>'; |
| 556 | |
| 557 | $html .= '<div style="padding:5px 0 12px 0;display:flex;align-items:center;">'; |
| 558 | $html .= '<a target="_blank" class="button button-primary donadosu-action-button" data-action="reviewed" style="margin-right:10px;" href="' . esc_url( $review_url ) . '">✏️ ' . |
| 559 | esc_html__( 'Write Review', 'donateocean-donation-suite' ) . '</a>'; |
| 560 | $html .= '<button type="button" class="button button-secondary donadosu-action-button" data-action="never" style="margin-right:10px;">✌️ ' . |
| 561 | esc_html__( 'Done!', 'donateocean-donation-suite' ) . '</button>'; |
| 562 | $html .= '<div style="flex:auto;"></div>'; |
| 563 | $html .= '<button type="button" class="button button-secondary donadosu-action-button" data-action="later" style="margin-right:10px;">⏰ ' . |
| 564 | esc_html__( 'Remind me later', 'donateocean-donation-suite' ) . '</button>'; |
| 565 | $html .= '<a href="#" class="button-link donadosu-action-button" data-action="never" style="font-size:small;">' . |
| 566 | esc_html__( 'Hide', 'donateocean-donation-suite' ) . '</a>'; |
| 567 | $html .= '</div>'; |
| 568 | $html .= '</div>'; |
| 569 | |
| 570 | echo wp_kses( $html, array( |
| 571 | 'div' => array( 'class' => array(), 'style' => array() ), |
| 572 | 'p' => array( 'style' => array() ), |
| 573 | 'h2' => array( 'class' => array(), 'style' => array() ), |
| 574 | 'b' => array(), |
| 575 | 'br' => array(), |
| 576 | 'a' => array( 'href' => array(), 'target' => array(), 'class' => array(), 'style' => array(), 'data-action' => array() ), |
| 577 | 'button' => array( 'type' => array(), 'class' => array(), 'style' => array(), 'data-action' => array() ), |
| 578 | 'span' => array( 'style' => array(), 'class' => array() ), |
| 579 | ) ); |
| 580 | } |
| 581 | |
| 582 | public function handle_action(): void { |
| 583 | check_ajax_referer( self::NONCE, 'nonce' ); |
| 584 | if ( ! current_user_can( 'manage_options' ) ) { |
| 585 | wp_send_json_error( 'Unauthorized', 403 ); |
| 586 | } |
| 587 | |
| 588 | $action = isset( $_POST['review_action'] ) ? sanitize_text_field( wp_unslash( $_POST['review_action'] ) ) : ''; |
| 589 | |
| 590 | if ( 'later' === $action ) { |
| 591 | update_option( self::NEXT_OPTION, time() + ( 7 * DAY_IN_SECONDS ) ); // snooze 7 days |
| 592 | update_option( self::HIDE_OPTION, 'later' ); |
| 593 | } elseif ( 'never' === $action || 'reviewed' === $action ) { |
| 594 | update_option( self::HIDE_OPTION, 'never' ); |
| 595 | } else { |
| 596 | wp_send_json_error( 'Invalid action' ); |
| 597 | } |
| 598 | |
| 599 | wp_send_json_success(); |
| 600 | } |
| 601 | |
| 602 | public function enqueue(): void { |
| 603 | if ( ! $this->is_settings_screen() ) { |
| 604 | return; |
| 605 | } |
| 606 | wp_enqueue_script( |
| 607 | 'donadosu-review', |
| 608 | DONADOSU_URL . 'assets/js/review-ajax.js', |
| 609 | array( 'jquery' ), |
| 610 | DONADOSU_VERSION, |
| 611 | true |
| 612 | ); |
| 613 | wp_localize_script( 'donadosu-review', 'donadosuReview', array( |
| 614 | 'ajax_url' => admin_url( 'admin-ajax.php' ), |
| 615 | 'nonce' => wp_create_nonce( self::NONCE ), |
| 616 | 'review_url' => self::REVIEW_URL, |
| 617 | ) ); |
| 618 | } |
| 619 | |
| 620 | /** Allow the inline flex/spacing styles used in the notice through wp_kses. */ |
| 621 | public function allowed_css( $styles ) { |
| 622 | return array_merge( $styles, array( 'display', 'align-items', 'flex', 'auto', 'margin', 'padding' ) ); |
| 623 | } |
| 624 | } |
| 625 | ``` |
| 626 | |
| 627 | ### B.2 — `assets/js/review-ajax.js` |
| 628 | |
| 629 | ```js |
| 630 | jQuery(function ($) { |
| 631 | $('.donadosu-action-button').on('click', function (e) { |
| 632 | $('.donadosu-review-notice').fadeOut(); |
| 633 | e.preventDefault(); |
| 634 | |
| 635 | const action = $(this).data('action'); |
| 636 | const reviewUrl = (typeof donadosuReview !== 'undefined' && donadosuReview.review_url) |
| 637 | ? donadosuReview.review_url |
| 638 | : 'https://wordpress.org/support/plugin/donateocean-donation-suite/reviews/#new-post'; |
| 639 | |
| 640 | if (action === 'reviewed') { |
| 641 | window.open(reviewUrl, '_blank'); |
| 642 | } |
| 643 | |
| 644 | $.post(donadosuReview.ajax_url, { |
| 645 | action: 'donadosu_handle_review_action', |
| 646 | review_action: action, |
| 647 | nonce: donadosuReview.nonce |
| 648 | }, function (response) { |
| 649 | if (!(response && response.success)) { |
| 650 | console.error(response && response.data ? response.data : 'Unknown error'); |
| 651 | } |
| 652 | }); |
| 653 | }); |
| 654 | }); |
| 655 | ``` |
| 656 | |
| 657 | --- |
| 658 | |
| 659 | # PART C — Register both classes in `Bootstrap` |
| 660 | |
| 661 | Open `includes/core/class-bootstrap.php`. At the top, alongside the other admin `use` statements |
| 662 | (e.g. `use DonationSuite\Admin\SettingsPage;`), add: |
| 663 | |
| 664 | ```php |
| 665 | use DonationSuite\Admin\DeactivationFeedback; |
| 666 | use DonationSuite\Admin\ReviewNotice; |
| 667 | ``` |
| 668 | |
| 669 | Then inside the `services()` method, next to the existing `( new SettingsPage() )->register();` |
| 670 | lines, add: |
| 671 | |
| 672 | ```php |
| 673 | ( new DeactivationFeedback() )->register(); |
| 674 | ( new ReviewNotice() )->register(); |
| 675 | ``` |
| 676 | |
| 677 | That's all the wiring needed — the autoloader resolves the class files, and each class's |
| 678 | `register()` adds its own hooks (and early‑returns on non‑admin requests). |
| 679 | |
| 680 | --- |
| 681 | |
| 682 | # PART D — (Optional) "Rate this plugin" row‑meta link |
| 683 | |
| 684 | If DonateOcean doesn't already add review/support links under its plugin row, you can add them in the |
| 685 | main file: |
| 686 | |
| 687 | ```php |
| 688 | add_filter( 'plugin_row_meta', 'donadosu_plugin_meta_links', 10, 2 ); |
| 689 | function donadosu_plugin_meta_links( $meta, $file ) { |
| 690 | if ( plugin_basename( DONADOSU_FILE ) === $file ) { |
| 691 | $meta[] = '<a href="https://wordpress.org/support/plugin/donateocean-donation-suite/">' . __( 'Community Support', 'donateocean-donation-suite' ) . '</a>'; |
| 692 | $meta[] = '<a href="https://wordpress.org/support/plugin/donateocean-donation-suite/reviews/#new-post" target="_blank" rel="noopener noreferrer">' . __( 'Rate this Plugin', 'donateocean-donation-suite' ) . '</a>'; |
| 693 | } |
| 694 | return $meta; |
| 695 | } |
| 696 | ``` |
| 697 | |
| 698 | --- |
| 699 | |
| 700 | # PART E — Uninstall cleanup |
| 701 | |
| 702 | `uninstall.php` already deletes `donadosu_activated_at` in its "always executed" block. Add the two |
| 703 | review options there too (these are tiny, so cleaning them regardless of the `cleanup_on_uninstall` |
| 704 | setting is fine): |
| 705 | |
| 706 | ```php |
| 707 | delete_option( 'donadosu_review_notice_hide' ); |
| 708 | delete_option( 'donadosu_review_next_show' ); |
| 709 | ``` |
| 710 | |
| 711 | > No cleanup is needed for the deactivation feedback feature — it stores nothing locally. |
| 712 | |
| 713 | --- |
| 714 | |
| 715 | # Integration checklist |
| 716 | |
| 717 | **Setup** |
| 718 | |
| 719 | - [ ] `DONADOSU_BASENAME` constant added to `donateocean-donation-suite.php`. |
| 720 | - [ ] `feedback/fonts/icomoon.*` copied from the reference plugin to `assets/fonts/`. |
| 721 | |
| 722 | **Deactivation feedback (Part A)** |
| 723 | |
| 724 | - [ ] `includes/admin/class-deactivation-feedback.php` created (`DonationSuite\Admin\DeactivationFeedback`). |
| 725 | - [ ] `includes/admin/views/deactivation-feedback-form.php` created. |
| 726 | - [ ] `assets/js/deactivation-feedback-modal.js` created — `data-slug="donateocean-donation-suite"`. |
| 727 | - [ ] `assets/css/deactivation-feedback-modal.css` created (prefix → `donadosu-deactivation-Modal`). |
| 728 | - [ ] Airtable base ID + token set (or pointed at your own storage); `'plugin' => 'DonateOcean'`. |
| 729 | |
| 730 | **Review notice (Part B)** |
| 731 | |
| 732 | - [ ] `includes/admin/class-review-notice.php` created (`DonationSuite\Admin\ReviewNotice`). |
| 733 | - [ ] `assets/js/review-ajax.js` created. |
| 734 | |
| 735 | **Wiring (Part C)** |
| 736 | |
| 737 | - [ ] `use` statements + two `( new … )->register();` calls added to `Bootstrap::services()`. |
| 738 | |
| 739 | **Cleanup (Part E)** |
| 740 | |
| 741 | - [ ] Two review options added to `uninstall.php`. |
| 742 | |
| 743 | --- |
| 744 | |
| 745 | # Testing |
| 746 | |
| 747 | **Deactivation modal** |
| 748 | |
| 749 | 1. **Plugins** screen → click **Deactivate** on DonateOcean → modal opens (click intercepted, plugin |
| 750 | not yet deactivated). |
| 751 | 2. Pick a reason; for reasons with a textarea, **Send & Deactivate** enables only after text entry. |
| 752 | 3. **Send & Deactivate** → an Airtable row appears, then the plugin deactivates. |
| 753 | 4. **I rather wouldn't say** → still deactivates (sends a generic reason). |
| 754 | 5. Confirm none of the modal assets load on screens other than `plugins.php`. |
| 755 | |
| 756 | **Review notice** |
| 757 | |
| 758 | 1. To see it immediately while testing, backdate activation and clear dismissal: |
| 759 | ```php |
| 760 | update_option( 'donadosu_activated_at', time() - DAY_IN_SECONDS - 60 ); |
| 761 | delete_option( 'donadosu_review_notice_hide' ); |
| 762 | delete_option( 'donadosu_review_next_show' ); |
| 763 | ``` |
| 764 | 2. Visit **Donation Suite → Settings** (`page=donadosu-settings`) → the notice appears. |
| 765 | 3. **Remind me later** → hides; should not return for 7 days (`donadosu_review_next_show` ≈ now+7d, |
| 766 | `donadosu_review_notice_hide` = `later`). |
| 767 | 4. **Hide** / **Done!** / **Write Review** → `donadosu_review_notice_hide` = `never`; never returns. |
| 768 | **Write Review** also opens the WordPress.org review page in a new tab. |
| 769 | 5. Confirm the notice does not appear on other admin screens or for users lacking `manage_options`. |
| 770 | |
| 771 | --- |
| 772 | |
| 773 | # Security & privacy notes |
| 774 | |
| 775 | - **Nonces:** deactivation handler verifies `donadosu-deactivation`; review handler verifies |
| 776 | `donadosu_review_nonce`. Keep each in sync between its `wp_localize_script` and `check_ajax_referer`. |
| 777 | (You may instead reuse the settings page's existing `donadosu_admin_actions` nonce / `donadosuAdmin` |
| 778 | object if you'd rather not add new ones — just align the JS accordingly.) |
| 779 | - **Capabilities:** deactivation handler requires `activate_plugins`; review handler requires |
| 780 | `manage_options` (matching the settings page). Both reject otherwise. |
| 781 | - **POST‑only + sanitize:** the deactivation handler rejects non‑POST requests and sanitizes every |
| 782 | input with `sanitize_text_field( wp_unslash( … ) )`. |
| 783 | - **No PII:** only the chosen reason + environment info (PHP/WP/WC versions, locale, theme, |
| 784 | multisite) is transmitted — matching the privacy note shown in the modal. |
| 785 | - **Airtable token:** the reference plugin hardcodes its Airtable Personal Access Token in PHP. A |
| 786 | token shipped in plugin code is readable by anyone with the files. Prefer **not** to hardcode it — |
| 787 | define it via a constant set outside the repo, route through a server‑side proxy, or store it in a |
| 788 | site option. At minimum scope the token to a single base with write‑only access and rotate it if it |
| 789 | leaks. If you reuse the team's existing base, only the `'plugin'` field value changes |
| 790 | (`'DonateOcean'`) so each plugin's feedback stays distinguishable. |
| 791 | |
| 792 | --- |
| 793 | |
| 794 | ## Appendix — Where each feature lives in the reference plugin |
| 795 | |
| 796 | | Feature | Reference plugin location | |
| 797 | | ---------------------- | ------------------------- | |
| 798 | | Deactivation admin class | `payment-gateway-for-authorizenet-for-woocommerce-admin.php` (`EASYAUTHNET_AuthorizeNet_Admin`) | |
| 799 | | Deactivation modal markup | `feedback/deactivation-feedback-form.php` | |
| 800 | | Deactivation CSS / JS / fonts | `feedback/css/…`, `feedback/js/…`, `feedback/fonts/…` | |
| 801 | | Review notice methods | `includes/class-easy-payment-authorizenet-gateway.php` (`easyauthnet_leaverev`, `easyauthnet_handle_review_action`, `easyauthnet_enqueue_scripts`, `easyauthnet_allowed_css_properties`) | |
| 802 | | Review notice JS | `assets/js/easyauthnet-review-ajax.js` | |
| 803 | | "Rate this Plugin" link | `payment-gateway-for-authorizenet-for-woocommerce.php` (`easyauthnet_authorizenet_plugin_meta_links`) | |
| 804 |