PluginProbe ʕ •ᴥ•ʔ
Payment Gateway for Authorize.net for WooCommerce / 1.0.14
Payment Gateway for Authorize.net for WooCommerce v1.0.14
1.0.19 1.0.18 1.0.17 1.0.16 1.0.15 1.0.14 1.0.13 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
payment-gateway-for-authorize-net-for-woocommerce / docs / deactivation-feedback-and-review-implementation-guide.md
payment-gateway-for-authorize-net-for-woocommerce / docs Last commit date
deactivation-feedback-and-review-implementation-guide.md 3 weeks ago
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&#8217; uh?' );
237 $donadosu_deactivation_url = wp_nonce_url(
238 'plugins.php?action=deactivate&amp;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