| 1 |
<?php |
| 2 |
/** |
| 3 |
* Post-import feedback widget (spec 027-post-import-feedback). |
| 4 |
* |
| 5 |
* Extracted verbatim from includes/Core/Admin.php — behavior byte-identical (same |
| 6 |
* eligibility check, same enqueue handle/localize global-var name `templately` so |
| 7 |
* the widget's `window.templately` read is unaffected, same mount-point markup). |
| 8 |
* Two pieces were surgically lifted out of a larger class: |
| 9 |
* - the `'index.php' === $hook` early-return branch inside Admin::scripts() |
| 10 |
* (hooked here to admin_enqueue_scripts independently) |
| 11 |
* - Admin::my_custom_footer_html() (hooked here to admin_footer independently) |
| 12 |
* Neither block referenced `$this->` state from Admin.php — confirmed via grep |
| 13 |
* before extraction, so this is a clean lift, not a rewrite. The enqueued filenames |
| 14 |
* changed (js/dashboard.js -> js/post-import-feedback.js, css/dashboard-style.css -> |
| 15 |
* css/post-import-feedback-style.css) to match the renamed webpack entry (spec 027's |
| 16 |
* own Build Artifacts note); the WordPress script/style HANDLE and the localized JS |
| 17 |
* global variable name both stay 'templately' — react-src's widget entry reads |
| 18 |
* `window.templately` directly, so renaming either would break data delivery. |
| 19 |
* |
| 20 |
* FR-005 rolling 30-day cooldown (2026-07-16): eligibility used to be a one-shot |
| 21 |
* `$is_complete !== 'done'` check (no expiry once dismissed). It is now |
| 22 |
* time-boxed via `is_eligible()` — see that method's docblock for the migration |
| 23 |
* handling of the old no-expiry `'done'` sentinel. |
| 24 |
* |
| 25 |
* @package Templately |
| 26 |
*/ |
| 27 |
|
| 28 |
namespace Templately\Modules\PostImportFeedback; |
| 29 |
|
| 30 |
use Templately\Utils\Options; |
| 31 |
|
| 32 |
class Widget { |
| 33 |
|
| 34 |
/** |
| 35 |
* User-meta key holding the Unix timestamp of the last time this user |
| 36 |
* submitted or dismissed the feedback widget. Drives the FR-005 30-day |
| 37 |
* rolling cooldown, replacing the old no-expiry `'done'` sentinel |
| 38 |
* previously stored (and still legacy-read) in `templately_fsi_complete`. |
| 39 |
*/ |
| 40 |
const LAST_SHOWN_META = 'templately_feedback_last_shown'; |
| 41 |
|
| 42 |
/** FR-005 cooldown window: 30 days. */ |
| 43 |
const COOLDOWN_SECONDS = 30 * DAY_IN_SECONDS; |
| 44 |
|
| 45 |
public function enqueue( $hook = '' ) { |
| 46 |
if ( 'index.php' !== $hook ) { |
| 47 |
return; |
| 48 |
} |
| 49 |
|
| 50 |
if ( ! self::is_eligible() || wp_is_mobile() ) { |
| 51 |
return; |
| 52 |
} |
| 53 |
|
| 54 |
$user = Options::get_instance()->get( 'user' ); |
| 55 |
$email = isset( $user['email'] ) ? $user['email'] : ''; |
| 56 |
|
| 57 |
templately()->assets->enqueue( 'templately', 'css/post-import-feedback-style.css', [] ); |
| 58 |
templately()->assets->enqueue( 'templately', 'js/post-import-feedback.js', [], true ); |
| 59 |
templately()->assets->localize( 'templately', 'templately', [ |
| 60 |
'email' => $email, |
| 61 |
'nonce' => wp_create_nonce( 'templately_nonce' ), |
| 62 |
] ); |
| 63 |
} |
| 64 |
|
| 65 |
public function render_mount_point() { |
| 66 |
global $current_screen; |
| 67 |
|
| 68 |
if ( ! $current_screen ) { |
| 69 |
return false; |
| 70 |
} |
| 71 |
|
| 72 |
if ( $current_screen->id !== 'dashboard' ) { |
| 73 |
return false; |
| 74 |
} |
| 75 |
|
| 76 |
if ( self::is_eligible() && ! wp_is_mobile() ) : |
| 77 |
?> |
| 78 |
<div id="templately-fsi-feedback"></div> |
| 79 |
<?php |
| 80 |
endif; |
| 81 |
} |
| 82 |
|
| 83 |
/** |
| 84 |
* Whether the feedback widget should be shown to the given (or current) |
| 85 |
* user, per FR-005: suppressed while the user has submitted or skipped |
| 86 |
* feedback within the past 30 days; eligible again once that window |
| 87 |
* elapses. Requires `templately_fsi_complete` (an FSI has completed) to |
| 88 |
* be truthy at all — an entirely fresh install/user is never eligible. |
| 89 |
* |
| 90 |
* Migration handling for the old no-expiry `'done'` sentinel: on |
| 91 |
* installs from before this cooldown existed, `templately_fsi_complete` |
| 92 |
* may already be `'done'` with no `LAST_SHOWN_META` recorded. The same |
| 93 |
* shape also occurs going forward for a legitimate reason outside this |
| 94 |
* module's scope — `FullSiteImport\Concerns\RunsImport` writes the |
| 95 |
* `'done'` sentinel directly (not via this module) whenever the backend |
| 96 |
* reports feedback was already given for the pack. Both cases are |
| 97 |
* indistinguishable from here, so both are handled the same, |
| 98 |
* conservatively: do NOT immediately re-prompt (a bare "value isn't a |
| 99 |
* timestamp" read should never suddenly show the widget to someone who |
| 100 |
* already dismissed it) — treat it as "just shown", backfilling |
| 101 |
* `LAST_SHOWN_META` to now so the 30-day clock starts running instead of |
| 102 |
* suppressing forever. |
| 103 |
*/ |
| 104 |
public static function is_eligible( $user_id = null ): bool { |
| 105 |
$user_id = $user_id ?: get_current_user_id(); |
| 106 |
|
| 107 |
$is_complete = get_user_meta( $user_id, 'templately_fsi_complete', true ); |
| 108 |
if ( empty( $is_complete ) ) { |
| 109 |
return false; |
| 110 |
} |
| 111 |
|
| 112 |
$last_shown = get_user_meta( $user_id, self::LAST_SHOWN_META, true ); |
| 113 |
|
| 114 |
if ( 'done' === $is_complete && empty( $last_shown ) ) { |
| 115 |
update_user_meta( $user_id, self::LAST_SHOWN_META, time() ); |
| 116 |
return false; |
| 117 |
} |
| 118 |
|
| 119 |
if ( empty( $last_shown ) ) { |
| 120 |
return true; |
| 121 |
} |
| 122 |
|
| 123 |
return ( time() - (int) $last_shown ) >= self::COOLDOWN_SECONDS; |
| 124 |
} |
| 125 |
} |
| 126 |
|