| 1 |
<?php |
| 2 |
/** |
| 3 |
* The one-time ask: would you show a credit on your confirmation page? |
| 4 |
* |
| 5 |
* Asked once, after the plugin has actually done something, and never again |
| 6 |
* either way. |
| 7 |
* |
| 8 |
* The shape of this notice is deliberate, because the tempting version is the |
| 9 |
* one that gets a plugin thrown out of the directory. Guideline 10 requires the |
| 10 |
* choice to be made through "clearly stated and understandable choices, not |
| 11 |
* buried in the terms of use or documentation" — so: both answers are ordinary |
| 12 |
* buttons of the same weight, the wording says plainly what will appear and |
| 13 |
* where, and the preview shows the exact markup rather than describing it. No |
| 14 |
* pre-selection, no dark pattern, no second asking. |
| 15 |
* |
| 16 |
* What it does lean on is legitimate and true: the plugin has confirmed a real, |
| 17 |
* countable number of opt-ins for this site before it asks for anything, and it |
| 18 |
* says why the link helps. That is the whole of the persuasion, and it is the |
| 19 |
* only part worth having — a site owner tricked into it removes it the moment |
| 20 |
* they notice, and leaves a one-star review on the way out. |
| 21 |
*/ |
| 22 |
|
| 23 |
namespace forge12\contactform7\CF7DoubleOptIn; |
| 24 |
|
| 25 |
if ( ! defined( 'ABSPATH' ) ) { |
| 26 |
exit; |
| 27 |
} |
| 28 |
|
| 29 |
/** |
| 30 |
* Confirmed opt-ins required before the question is asked at all. |
| 31 |
* |
| 32 |
* High enough that the plugin has demonstrably earned the ask, low enough that |
| 33 |
* an active site reaches it in a sensible time. Lower than the captcha plugin's |
| 34 |
* equivalent (100 blocked spam submissions) because a confirmed opt-in is a |
| 35 |
* much rarer event than a blocked spam attempt. |
| 36 |
*/ |
| 37 |
const CREDIT_NUDGE_THRESHOLD = 50; |
| 38 |
|
| 39 |
/** |
| 40 |
* Option holding the answer. Absent = not asked yet, 'accepted' / 'declined' = done. |
| 41 |
*/ |
| 42 |
const CREDIT_NUDGE_ANSWER_OPTION = 'f12_doi_credit_nudge_answer'; |
| 43 |
|
| 44 |
/** |
| 45 |
* How many opt-ins this install has confirmed. |
| 46 |
* |
| 47 |
* The same counter the review notice uses, so the two never tell the site owner |
| 48 |
* different numbers for the same thing. |
| 49 |
*/ |
| 50 |
function get_confirmed_count(): int { |
| 51 |
$counters = get_option( 'f12_cf7_doubleoptin_telemetry_counters', array() ); |
| 52 |
|
| 53 |
return ( is_array( $counters ) && isset( $counters['confirmed_optins'] ) ) |
| 54 |
? (int) $counters['confirmed_optins'] |
| 55 |
: 0; |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* Whether to ask on this screen. |
| 60 |
*/ |
| 61 |
function should_show_credit_nudge(): bool { |
| 62 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 63 |
return false; |
| 64 |
} |
| 65 |
|
| 66 |
// Answered once, in either direction: never ask again. |
| 67 |
if ( get_option( CREDIT_NUDGE_ANSWER_OPTION, '' ) !== '' ) { |
| 68 |
return false; |
| 69 |
} |
| 70 |
|
| 71 |
// Already on — nothing to ask for. Covers the site owner who found the |
| 72 |
// setting themselves. |
| 73 |
if ( is_credit_enabled() ) { |
| 74 |
return false; |
| 75 |
} |
| 76 |
|
| 77 |
if ( get_confirmed_count() < CREDIT_NUDGE_THRESHOLD ) { |
| 78 |
return false; |
| 79 |
} |
| 80 |
|
| 81 |
// Never alongside the review notice. Two requests on one screen is the |
| 82 |
// nagging that earns one-star reviews, and the review is the more valuable |
| 83 |
// of the two. |
| 84 |
if ( ! empty( $GLOBALS['f12_doi_review_notice_shown'] ) ) { |
| 85 |
return false; |
| 86 |
} |
| 87 |
|
| 88 |
return true; |
| 89 |
} |
| 90 |
|
| 91 |
/** |
| 92 |
* Render the notice. |
| 93 |
*/ |
| 94 |
function render_credit_nudge(): void { |
| 95 |
if ( ! should_show_credit_nudge() ) { |
| 96 |
return; |
| 97 |
} |
| 98 |
|
| 99 |
$confirmed = get_confirmed_count(); |
| 100 |
$accept = wp_nonce_url( add_query_arg( 'f12_doi_credit', 'yes' ), 'f12_doi_credit' ); |
| 101 |
$decline = wp_nonce_url( add_query_arg( 'f12_doi_credit', 'no' ), 'f12_doi_credit' ); |
| 102 |
?> |
| 103 |
<div class="notice notice-info f12-doi-credit-nudge"> |
| 104 |
<p> |
| 105 |
<?php |
| 106 |
printf( |
| 107 |
wp_kses( |
| 108 |
/* translators: %s: number of confirmed opt-ins, already formatted. */ |
| 109 |
__( 'Double Opt-In has confirmed <strong>%s opt-ins</strong> on this site so far.', 'double-opt-in' ), |
| 110 |
array( 'strong' => array() ) |
| 111 |
), |
| 112 |
esc_html( number_format_i18n( $confirmed ) ) |
| 113 |
); |
| 114 |
?> |
| 115 |
<?php esc_html_e( 'Would you show a small credit on your confirmation page? It helps other site owners find the plugin, and it is what keeps it free.', 'double-opt-in' ); ?> |
| 116 |
</p> |
| 117 |
|
| 118 |
<p style="margin:0 0 4px;"><?php esc_html_e( 'This is exactly what would appear, on the page a subscriber lands on after confirming:', 'double-opt-in' ); ?></p> |
| 119 |
<div style="padding:8px 12px;background:#fff;border:1px solid #dcdcde;border-radius:3px;display:inline-block;margin-bottom:8px;"> |
| 120 |
<?php |
| 121 |
// Rendered, not described: the real objection is "will this clutter |
| 122 |
// my page", and only the actual thing answers it. |
| 123 |
echo wp_kses( |
| 124 |
get_credit_markup( true ), |
| 125 |
array( |
| 126 |
'p' => array( 'class' => true ), |
| 127 |
'a' => array( |
| 128 |
'href' => true, |
| 129 |
'target' => true, |
| 130 |
'rel' => true, |
| 131 |
), |
| 132 |
) |
| 133 |
); |
| 134 |
?> |
| 135 |
</div> |
| 136 |
|
| 137 |
<p> |
| 138 |
<a href="<?php echo esc_url( $accept ); ?>" class="button button-primary"> |
| 139 |
<?php esc_html_e( 'Yes, show the link', 'double-opt-in' ); ?> |
| 140 |
</a> |
| 141 |
<a href="<?php echo esc_url( $decline ); ?>" class="button"> |
| 142 |
<?php esc_html_e( 'No thanks', 'double-opt-in' ); ?> |
| 143 |
</a> |
| 144 |
<span style="margin-left:8px;color:#646970;"> |
| 145 |
<?php esc_html_e( 'Asked once. You can change it any time under Settings.', 'double-opt-in' ); ?> |
| 146 |
</span> |
| 147 |
</p> |
| 148 |
</div> |
| 149 |
<?php |
| 150 |
} |
| 151 |
|
| 152 |
/** |
| 153 |
* Record the answer. |
| 154 |
* |
| 155 |
* Nonce-checked and capability-checked: unlike a dismiss flag, "yes" writes a |
| 156 |
* setting that changes what every visitor of the confirmation page sees, so a |
| 157 |
* stray link must not be able to trigger it. |
| 158 |
*/ |
| 159 |
function handle_credit_nudge_answer(): void { |
| 160 |
if ( ! isset( $_GET['f12_doi_credit'] ) ) { |
| 161 |
return; |
| 162 |
} |
| 163 |
|
| 164 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 165 |
return; |
| 166 |
} |
| 167 |
|
| 168 |
check_admin_referer( 'f12_doi_credit' ); |
| 169 |
|
| 170 |
$answer = sanitize_text_field( wp_unslash( $_GET['f12_doi_credit'] ) ); |
| 171 |
|
| 172 |
if ( $answer === 'yes' ) { |
| 173 |
$settings = get_option( 'f12-doi-settings', array() ); |
| 174 |
if ( ! is_array( $settings ) ) { |
| 175 |
$settings = array(); |
| 176 |
} |
| 177 |
$settings[ CREDIT_SETTING_KEY ] = 1; |
| 178 |
update_option( 'f12-doi-settings', $settings ); |
| 179 |
|
| 180 |
update_option( CREDIT_NUDGE_ANSWER_OPTION, 'accepted' ); |
| 181 |
} else { |
| 182 |
update_option( CREDIT_NUDGE_ANSWER_OPTION, 'declined' ); |
| 183 |
} |
| 184 |
|
| 185 |
// Drop the parameters so a refresh does not replay the action. |
| 186 |
wp_safe_redirect( remove_query_arg( array( 'f12_doi_credit', '_wpnonce' ) ) ); |
| 187 |
exit; |
| 188 |
} |
| 189 |
|
| 190 |
// Priority 20: after the review notice, so the guard in |
| 191 |
// should_show_credit_nudge() can see whether that one already claimed this screen. |
| 192 |
add_action( 'admin_notices', __NAMESPACE__ . '\render_credit_nudge', 20 ); |
| 193 |
add_action( 'admin_init', __NAMESPACE__ . '\handle_credit_nudge_answer' ); |
| 194 |
|