Skip to content

Commit c7115d9

Browse files
committed
Add marketing opt-out handling for Freemius webhooks
Add respect_marketing_optout setting to enable/disable marketing opt-out handling Add marketing_optout column to subscribers table for tracking opt-out status Implement process_marketing_optout() to handle user.marketing.opted_out events Record opt-out in database and unsubscribe from Kit when opted out Block future subscriptions for opted-out users Add unsubscribe_subscriber() method to Kit_API wrapper Fix text domain in Admin class banner
1 parent 989b23c commit c7115d9

7 files changed

Lines changed: 195 additions & 51 deletions

File tree

includes/admin/class-admin.php

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -82,42 +82,7 @@ class Admin {
8282
public function __construct( Database $database ) {
8383
$this->database = $database;
8484
self::$notices_api = new Admin_Notices_API();
85-
$this->admin_banner = new Admin_Banner(
86-
array(
87-
'capability' => 'manage_options',
88-
'prefix' => 'freemkit',
89-
'strings' => array(
90-
'region_label' => __( 'FreemKit admin navigation', 'freemkit' ),
91-
'nav_label' => __( 'FreemKit sections', 'freemkit' ),
92-
'eyebrow' => __( 'WebberZone', 'freemkit' ),
93-
'title' => __( 'FreemKit', 'freemkit' ),
94-
'text' => __( 'Manage integration settings and subscribers.', 'freemkit' ),
95-
),
96-
'sections' => array(
97-
'settings' => array(
98-
'label' => __( 'Settings', 'freemkit' ),
99-
'url' => admin_url( 'options-general.php?page=freemkit_options_page' ),
100-
'type' => 'primary',
101-
'page_slugs' => array( 'freemkit_options_page' ),
102-
'screen_ids' => array( 'settings_page_freemkit_options_page' ),
103-
),
104-
'wizard' => array(
105-
'label' => __( 'Setup Wizard', 'freemkit' ),
106-
'url' => admin_url( 'options-general.php?page=freemkit_setup_wizard' ),
107-
'type' => 'secondary',
108-
'page_slugs' => array( 'freemkit_setup_wizard' ),
109-
'screen_ids' => array( 'settings_page_freemkit_setup_wizard' ),
110-
),
111-
'subscribers' => array(
112-
'label' => __( 'Subscribers', 'freemkit' ),
113-
'url' => admin_url( 'users.php?page=freemkit_subscribers' ),
114-
'type' => 'secondary',
115-
'page_slugs' => array( 'freemkit_subscribers' ),
116-
'screen_ids' => array( 'users_page_freemkit_subscribers' ),
117-
),
118-
),
119-
)
120-
);
85+
$this->admin_banner = new Admin_Banner( $this->get_admin_banner_config() );
12186

12287
// Initialize settings.
12388
$this->settings = new Settings();
@@ -177,4 +142,48 @@ public static function add_notice( $message, $notice_class = 'notice-info' ): vo
177142
)
178143
);
179144
}
145+
146+
/**
147+
* Retrieve the configuration array for the admin banner.
148+
*
149+
* @since 1.0.0
150+
*
151+
* @return array<string, mixed>
152+
*/
153+
private function get_admin_banner_config(): array {
154+
return array(
155+
'capability' => 'manage_options',
156+
'prefix' => 'freemkit',
157+
'strings' => array(
158+
'region_label' => __( 'FreemKit admin navigation', 'freemkit' ),
159+
'nav_label' => __( 'FreemKit sections', 'freemkit' ),
160+
'eyebrow' => __( 'WebberZone', 'freemkit' ),
161+
'title' => __( 'FreemKit', 'freemkit' ),
162+
'text' => __( 'Manage integration settings and subscribers.', 'freemkit' ),
163+
),
164+
'sections' => array(
165+
'settings' => array(
166+
'label' => __( 'Settings', 'freemkit' ),
167+
'url' => admin_url( 'options-general.php?page=freemkit_options_page' ),
168+
'type' => 'primary',
169+
'page_slugs' => array( 'freemkit_options_page' ),
170+
'screen_ids' => array( 'settings_page_freemkit_options_page' ),
171+
),
172+
'subscribers' => array(
173+
'label' => __( 'Subscribers', 'freemkit' ),
174+
'url' => admin_url( 'users.php?page=freemkit_subscribers' ),
175+
'type' => 'secondary',
176+
'page_slugs' => array( 'freemkit_subscribers' ),
177+
'screen_ids' => array( 'users_page_freemkit_subscribers' ),
178+
),
179+
'plugins' => array(
180+
'label' => esc_html__( 'WebberZone Plugins', 'freemkit' ),
181+
'url' => 'https://webberzone.com/plugins/',
182+
'type' => 'secondary',
183+
'target' => '_blank',
184+
'rel' => 'noopener noreferrer',
185+
),
186+
),
187+
);
188+
}
180189
}

includes/admin/class-settings-wizard.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public function get_wizard_steps(): array {
129129
'description' => __( 'Configure webhook handling and add one or more Freemius plugin mappings.', 'freemkit' ),
130130
'settings' => $this->build_step_settings(
131131
array(
132+
'respect_marketing_optout',
132133
'webhook_endpoint_type',
133134
'webhook_url',
134135
'plugins',

includes/admin/class-settings.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,13 +450,20 @@ public static function settings_kit(): array {
450450
*/
451451
public static function settings_subscribers(): array {
452452
$settings = array(
453-
'subscribers' => array(
453+
'subscribers' => array(
454454
'id' => 'subscribers',
455455
'name' => __( 'Subscribers', 'freemkit' ),
456456
'desc' => __( 'Configure your subscribers settings in this tab.', 'freemkit' ),
457457
'type' => 'header',
458458
),
459-
'last_name_field' => array(
459+
'respect_marketing_optout' => array(
460+
'id' => 'respect_marketing_optout',
461+
'name' => __( 'Respect Marketing Opt-out', 'freemkit' ),
462+
'desc' => __( 'When enabled, users who opt out of marketing on Freemius will be unsubscribed from Kit and blocked from future subscriptions.', 'freemkit' ),
463+
'type' => 'checkbox',
464+
'default' => 1,
465+
),
466+
'last_name_field' => array(
460467
'id' => 'last_name_field',
461468
'name' => __( 'Last Name field', 'freemkit' ),
462469
'desc' => __( 'Select the field name for mapping the last name in Kit. Note: Kit lacks a default last name field; a custom field must be created in your account first.', 'freemkit' ),
@@ -465,7 +472,7 @@ public static function settings_subscribers(): array {
465472
'field_class' => 'ts_autocomplete',
466473
'field_attributes' => self::get_kit_search_field_attributes( 'custom_fields', array( 'maxItems' => 1 ) ),
467474
),
468-
'custom_fields' => array(
475+
'custom_fields' => array(
469476
'id' => 'custom_fields',
470477
'name' => __( 'Custom Fields', 'freemkit' ),
471478
'desc' => '',

includes/class-database.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ public function create_table() {
7777
last_name varchar(50) DEFAULT '',
7878
fields longtext DEFAULT NULL,
7979
status varchar(20) NOT NULL DEFAULT 'active',
80+
marketing_optout tinyint(1) NOT NULL DEFAULT 0,
8081
created datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
8182
modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
8283
PRIMARY KEY (id),
8384
UNIQUE KEY email (email),
84-
KEY status (status)
85+
KEY status (status),
86+
KEY marketing_optout (marketing_optout)
8587
) {$charset_collate};";
8688

8789
$events_sql = "CREATE TABLE IF NOT EXISTS {$this->events_table_name} (
@@ -351,15 +353,16 @@ public function upsert_subscriber_by_email( $subscriber ) {
351353

352354
$sql = "
353355
INSERT INTO {$this->get_table_name()} (
354-
email, first_name, last_name, fields, status, created
356+
email, first_name, last_name, fields, status, marketing_optout, created
355357
) VALUES (
356-
%s, %s, %s, %s, %s, %s
358+
%s, %s, %s, %s, %s, %d, %s
357359
)
358360
ON DUPLICATE KEY UPDATE
359361
first_name = VALUES(first_name),
360362
last_name = VALUES(last_name),
361363
fields = VALUES(fields),
362364
status = VALUES(status),
365+
marketing_optout = VALUES(marketing_optout),
363366
modified = CURRENT_TIMESTAMP
364367
";
365368

@@ -371,6 +374,7 @@ public function upsert_subscriber_by_email( $subscriber ) {
371374
$data['data']['last_name'],
372375
$data['data']['fields'],
373376
$data['data']['status'],
377+
$data['data']['marketing_optout'],
374378
$data['data']['created']
375379
)
376380
);
@@ -499,20 +503,21 @@ public function update_subscriber( $subscriber ) {
499503
*/
500504
public function prepare_subscriber_data( $subscriber, $is_new = true ) {
501505
$data = array(
502-
'email' => sanitize_email( $subscriber->email ),
503-
'first_name' => sanitize_text_field( $subscriber->first_name ),
504-
'last_name' => sanitize_text_field( $subscriber->last_name ),
505-
'fields' => $this->prepare_array_field( $subscriber->fields ),
506-
'status' => ! empty( $subscriber->status ) ? $subscriber->status : 'active',
506+
'email' => sanitize_email( $subscriber->email ),
507+
'first_name' => sanitize_text_field( $subscriber->first_name ),
508+
'last_name' => sanitize_text_field( $subscriber->last_name ),
509+
'fields' => $this->prepare_array_field( $subscriber->fields ),
510+
'status' => ! empty( $subscriber->status ) ? $subscriber->status : 'active',
511+
'marketing_optout' => (int) $subscriber->marketing_optout,
507512
);
508513

509514
if ( $is_new ) {
510515
$data['created'] = ! empty( $subscriber->created )
511516
? $subscriber->created
512517
: current_time( 'mysql', true );
513-
$format = array( '%s', '%s', '%s', '%s', '%s', '%s' );
518+
$format = array( '%s', '%s', '%s', '%s', '%s', '%d', '%s' );
514519
} else {
515-
$format = array( '%s', '%s', '%s', '%s', '%s' );
520+
$format = array( '%s', '%s', '%s', '%s', '%s', '%d' );
516521
}
517522

518523
return array(

includes/class-subscriber.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ class Subscriber {
6363
*/
6464
public string $status = 'active';
6565

66+
/**
67+
* Marketing opt-out flag.
68+
*
69+
* @since 1.0.0
70+
* @var int
71+
*/
72+
public int $marketing_optout = 0;
73+
6674
/**
6775
* Created timestamp.
6876
*
@@ -153,7 +161,8 @@ public function init_by_data( array $data ) {
153161
if ( isset( $data[ $key ] ) ) {
154162
switch ( $key ) {
155163
case 'id':
156-
$this->id = (int) $data[ $key ];
164+
case 'marketing_optout':
165+
$this->$key = (int) $data[ $key ];
157166
break;
158167
case 'email':
159168
$this->email = $data[ $key ];

includes/class-webhook-handler.php

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,14 @@ public function process_webhook( string $input ) {
187187
return new \WP_Error( 'invalid_event', 'Missing event type in request.' );
188188
}
189189

190-
$event_type = Freemius::normalize_event_type( (string) $fs_event->type );
190+
$event_type = Freemius::normalize_event_type( (string) $fs_event->type );
191+
$respect_marketing_optout = (bool) Options_API::get_option( 'respect_marketing_optout' );
192+
193+
// Handle marketing opt-out event.
194+
if ( 'user.marketing.opted_out' === $event_type ) {
195+
return $this->process_marketing_optout( $email, $first_name, $last_name, $fields, $plugin_id, $plugin_config, $freemius_user_id, $respect_marketing_optout );
196+
}
197+
191198
$user_type = '';
192199
$active_form_ids = array();
193200
$active_tag_ids = array();
@@ -207,6 +214,20 @@ public function process_webhook( string $input ) {
207214
);
208215
}
209216

217+
// Block subscription if the subscriber has opted out of marketing.
218+
if ( $respect_marketing_optout ) {
219+
$existing = $this->database->get_subscriber_by_email( $email );
220+
if ( ! is_wp_error( $existing ) && ! empty( $existing->marketing_optout ) ) {
221+
// Safety: ensure they are unsubscribed from Kit.
222+
$this->api->unsubscribe_subscriber( $email );
223+
224+
return array(
225+
'status' => 'ignored',
226+
'message' => 'Subscriber has opted out of marketing; subscription blocked.',
227+
);
228+
}
229+
}
230+
210231
$api_result = $this->subscribe_to_forms( $active_form_ids, $email, $first_name, $fields, $active_tag_ids );
211232

212233
if ( is_wp_error( $api_result ) ) {
@@ -314,6 +335,83 @@ public function subscribe_to_forms( array $form_ids, string $email, string $firs
314335
return $result;
315336
}
316337

338+
/**
339+
* Process a marketing opt-out event.
340+
*
341+
* Records the opt-out in the database and unsubscribes from Kit.
342+
*
343+
* @since 1.0.0
344+
*
345+
* @param string $email Subscriber email.
346+
* @param string $first_name Subscriber first name.
347+
* @param string $last_name Subscriber last name.
348+
* @param array $fields Custom fields.
349+
* @param string $plugin_id Freemius plugin ID.
350+
* @param array $plugin_config Plugin configuration.
351+
* @param int $freemius_user_id Freemius user ID.
352+
* @param bool $respect_marketing_optout Whether the setting is enabled.
353+
* @return array|\WP_Error
354+
*/
355+
public function process_marketing_optout( string $email, string $first_name, string $last_name, array $fields, string $plugin_id, array $plugin_config, int $freemius_user_id, bool $respect_marketing_optout ) {
356+
if ( ! $respect_marketing_optout ) {
357+
return array(
358+
'status' => 'ignored',
359+
'message' => 'Marketing opt-out handling is disabled.',
360+
);
361+
}
362+
363+
$subscriber = new Subscriber(
364+
array(
365+
'email' => $email,
366+
'first_name' => $first_name,
367+
'last_name' => $last_name,
368+
'fields' => $fields,
369+
'status' => 'opted_out',
370+
'marketing_optout' => 1,
371+
)
372+
);
373+
374+
$db_result = $this->database->upsert_subscriber_by_email( $subscriber );
375+
if ( is_wp_error( $db_result ) ) {
376+
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
377+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
378+
error_log( sprintf( '[FreemKit] Database Error during marketing opt-out: %s', $db_result->get_error_message() ) );
379+
}
380+
return new \WP_Error( 'db_error', 'Marketing opt-out recorded with database errors' );
381+
}
382+
383+
$event = new Subscriber_Event(
384+
array(
385+
'subscriber_id' => $db_result,
386+
'plugin_id' => (string) $plugin_id,
387+
'plugin_slug' => $plugin_config['slug'],
388+
'event_type' => 'user.marketing.opted_out',
389+
'user_type' => 'opted_out',
390+
'form_ids' => '',
391+
'tag_ids' => '',
392+
'freemius_user_id' => $freemius_user_id,
393+
)
394+
);
395+
396+
$event_result = $this->database->add_subscriber_event( $event );
397+
if ( is_wp_error( $event_result ) && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
398+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
399+
error_log( sprintf( '[FreemKit] Event insert error during marketing opt-out: %s', $event_result->get_error_message() ) );
400+
}
401+
402+
// Unsubscribe from Kit.
403+
$unsubscribe_result = $this->api->unsubscribe_subscriber( $email );
404+
if ( is_wp_error( $unsubscribe_result ) && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
405+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
406+
error_log( sprintf( '[FreemKit] Kit unsubscribe error: %s', $unsubscribe_result->get_error_message() ) );
407+
}
408+
409+
return array(
410+
'status' => 'success',
411+
'message' => 'Marketing opt-out processed; subscriber unsubscribed from Kit.',
412+
);
413+
}
414+
317415
/**
318416
* Extract and validate signature from various possible header formats.
319417
*

includes/kit/class-kit-api.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,21 @@ public function subscribe_to_form( int $form_id, string $email, string $first_na
167167
return $result;
168168
}
169169

170+
/**
171+
* Unsubscribe a subscriber from Kit by email.
172+
*
173+
* @param string $email Email address.
174+
* @return array|\WP_Error|null
175+
*/
176+
public function unsubscribe_subscriber( string $email ) {
177+
$validate = $this->validate_email( $email );
178+
if ( is_wp_error( $validate ) ) {
179+
return $validate;
180+
}
181+
182+
return parent::unsubscribe_by_email( $email );
183+
}
184+
170185
/**
171186
* Validate email.
172187
*

0 commit comments

Comments
 (0)