Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(data-events): mailchimp connector #2233

Merged
merged 15 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions includes/class-newspack.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ private function includes() {
include_once NEWSPACK_ABSPATH . 'includes/data-events/class-webhooks.php';
include_once NEWSPACK_ABSPATH . 'includes/data-events/class-api.php';
include_once NEWSPACK_ABSPATH . 'includes/data-events/listeners.php';
include_once NEWSPACK_ABSPATH . 'includes/data-events/connectors/class-mailchimp.php';
include_once NEWSPACK_ABSPATH . 'includes/class-api.php';
include_once NEWSPACK_ABSPATH . 'includes/class-profile.php';
include_once NEWSPACK_ABSPATH . 'includes/analytics/class-analytics.php';
Expand Down
234 changes: 234 additions & 0 deletions includes/data-events/connectors/class-mailchimp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<?php
/**
* Newspack Data Events Mailchimp Connector
*
* @package Newspack
*/

namespace Newspack\Data_Events\Connectors;

use \Newspack\Data_Events;
use \Newspack\Mailchimp_API;
use \Newspack\Newspack_Newsletters;
use \Newspack\Reader_Activation;
use \Newspack\WooCommerce_Connection;
use \Newspack\Donations;

defined( 'ABSPATH' ) || exit;

/**
* Main Class.
*/
class Mailchimp {
/**
* Constructor.
*/
public function __construct() {
if (
defined( 'NEWSPACK_DATA_EVENTS_MAILCHIMP' ) && NEWSPACK_DATA_EVENTS_MAILCHIMP &&
Reader_Activation::is_enabled() &&
true === Reader_Activation::get_setting( 'sync_esp' )
) {
Data_Events::register_handler( [ __CLASS__, 'reader_registered' ], 'reader_registered' );
Data_Events::register_handler( [ __CLASS__, 'donation_new' ], 'donation_new' );
adekbadek marked this conversation as resolved.
Show resolved Hide resolved
Data_Events::register_handler( [ __CLASS__, 'donation_subscription_new' ], 'donation_subscription_new' );
}
}

/**
* Get audience ID.
*
* @return string|bool Audience ID or false if not set.
*/
private static function get_audience_id() {
/** TODO: UI for handling Mailchimp's master list in RAS. */
$audience_id = Reader_Activation::get_setting( 'mailchimp_audience_id' );
/** Attempt to use list ID from "Mailchimp for WooCommerce" */
if ( ! $audience_id && function_exists( 'mailchimp_get_list_id' ) ) {
$audience_id = mailchimp_get_list_id();
}
return ! empty( $audience_id ) ? $audience_id : false;
}

/**
* Get merge field type.
*
* @param mixed $value Value to check.
*
* @return string Merge field type.
*/
private static function get_merge_field_type( $value ) {
if ( is_numeric( $value ) ) {
return 'number';
}
if ( is_bool( $value ) ) {
return 'boolean';
}
return 'text';
}

/**
* Get merge fields given data.
*
* @param string $audience_id Audience ID.
* @param array $data Data to check.
*
* @return array Merge fields.
*/
private static function get_merge_fields( $audience_id, $data ) {
$merge_fields = [];
$fields_option_name = sprintf( 'newspack_data_mailchimp_%s_fields', $audience_id );

// Strip arrays.
$data = array_filter(
$data,
function( $value ) {
return ! is_array( $value );
}
);

// Get and match existing merge fields.
$fields_ids = \get_option( $fields_option_name, [] );
$existing_fields = Mailchimp_API::get( "lists/$audience_id/merge-fields?count=1000" );
foreach ( $existing_fields['merge_fields'] as $field ) {
$field_name = '';
if ( isset( $fields_ids[ $field['merge_id'] ] ) ) {
// Match locally stored merge field ID.
$field_name = $fields_ids[ $field['merge_id'] ];
} elseif ( isset( $data[ $field['name'] ] ) ) {
// Match by merge field name.
$field_name = $field['name'];
$fields_ids[ $field['merge_id'] ] = $field_name;
}
// If field name is found, add it to the payload.
if ( ! empty( $field_name ) && isset( $data[ $field_name ] ) ) {
$merge_fields[ $field['tag'] ] = $data[ $field_name ];
unset( $data[ $field_name ] );
}
}

// Create remaining fields.
$remaining_fields = array_keys( $data );
foreach ( $remaining_fields as $field_name ) {
$created_field = Mailchimp_API::post(
"lists/$audience_id/merge-fields",
[
'name' => $field_name,
'type' => self::get_merge_field_type( $data[ $field_name ] ),
]
);
$merge_fields[ $created_field['tag'] ] = $data[ $field_name ];
$fields_ids[ $created_field['merge_id'] ] = $field_name;
}

// Store fields IDs for future use.
\update_option( $fields_option_name, $fields_ids );
return $merge_fields;
}

/**
* Update a Mailchimp contact
*
* @param string $email Email address.
* @param array $data Data to update.
*/
private static function put( $email, $data = [] ) {
$audience_id = self::get_audience_id();
if ( ! $audience_id ) {
return;
}
$hash = md5( strtolower( $email ) );
$payload = [
'email_address' => $email,
'status_if_new' => 'transactional',
];

$merge_fields = self::get_merge_fields( $audience_id, $data );
if ( ! empty( $merge_fields ) ) {
$payload['merge_fields'] = $merge_fields;
}

// Upsert the contact.
Mailchimp_API::put(
"lists/$audience_id/members/$hash",
$payload
);
}

/**
* Handle a reader registering.
*
* @param int $timestamp Timestamp of the event.
* @param array $data Data associated with the event.
* @param int $client_id ID of the client that triggered the event.
*/
public static function reader_registered( $timestamp, $data, $client_id ) {
$metadata = [
'NP_Account' => $data['user_id'],
];
if ( isset( $data['metadata']['current_page_url'] ) ) {
$metadata['NP_Registration Page'] = $data['metadata']['current_page_url'];
}
if ( isset( $data['metadata']['registration_method'] ) ) {
$metadata['NP_Registration Method'] = $data['metadata']['registration_method'];
}
self::put( $data['email'], $metadata );
}

/**
* Handle a donation being made.
*
* @param int $timestamp Timestamp of the event.
* @param array $data Data associated with the event.
* @param int $client_id ID of the client that triggered the event.
*/
public static function donation_new( $timestamp, $data, $client_id ) {
if ( ! isset( $data['platform_data']['order_id'] ) ) {
return;
}

$order_id = $data['platform_data']['order_id'];
$contact = WooCommerce_Connection::get_contact_from_order( $order_id );

if ( ! $contact ) {
return;
}

$email = $contact['email'];
$metadata = $contact['metadata'];
$keys = Newspack_Newsletters::$metadata_keys;

// Only use metadata defined in 'Newspack_Newsletters'.
$metadata = array_intersect_key( $metadata, array_flip( $keys ) );

// Remove "product name" from metadata, we'll use
// 'donation_subscription_new' action for this data.
unset( $metadata[ $keys['product_name'] ] );

self::put( $email, $metadata );
}

/**
* Handle a new subscription.
*
* @param int $timestamp Timestamp of the event.
* @param array $data Data associated with the event.
* @param int $client_id ID of the client that triggered the event.
*/
public static function donation_subscription_new( $timestamp, $data, $client_id ) {
if ( empty( $data['platform_data']['order_id'] ) ) {
return;
}
$metadata = [
'NP_Account' => $data['user_id'],
];
$order_id = $data['platform_data']['order_id'];
$product_id = Donations::get_order_donation_product_id( $order_id );
$product_name = get_the_title( $product_id );

$metadata['NP_Product Name'] = $product_name;

self::put( $data['email'], $metadata );
}
}
new Mailchimp();
110 changes: 80 additions & 30 deletions includes/oauth/class-mailchimp-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,7 @@ public static function register_api_endpoints() {
* @return WP_REST_Response
*/
public static function api_mailchimp_auth_status() {
// 'newspack_mailchimp_api_key' is a new option introduced to manage MC API key accross Newspack plugins.
// Keeping the old option for backwards compatibility.
$mailchimp_api_key = get_option( 'newspack_mailchimp_api_key', get_option( 'newspack_newsletters_mailchimp_api_key' ) );
$endpoint = self::get_api_endpoint_from_key( $mailchimp_api_key );

if ( ! $mailchimp_api_key || ! $endpoint ) {
return \rest_ensure_response( [] );
}

$key_is_valid_response = self::is_valid_api_key( $endpoint, $mailchimp_api_key );

return $key_is_valid_response;
return self::is_valid_api_key();
}

/**
Expand All @@ -106,7 +95,7 @@ public static function api_mailchimp_save_key( $request ) {
return new \WP_Error( 'wrong_parameter', __( 'Invalid Mailchimp API Key.', 'newspack' ) );
}

$key_is_valid_response = self::is_valid_api_key( $endpoint, $request['api_key'] );
$key_is_valid_response = self::is_valid_api_key( $request['api_key'] );

if ( ! is_wp_error( $key_is_valid_response ) ) {
update_option( 'newspack_mailchimp_api_key', $request['api_key'] );
Expand Down Expand Up @@ -143,37 +132,98 @@ private static function get_api_endpoint_from_key( $api_key ) {
/**
* Check API Key validity
*
* @param string $endpoint Mailchimp API Endpoint.
* @param string $api_key Mailchimp API Key.
* @return WP_REST_Response|WP_Error
*/
private static function is_valid_api_key( $endpoint, $api_key ) {
private static function is_valid_api_key( $api_key = '' ) {
// Mailchimp API root endpoint returns details about the Mailchimp user account.
$response = wp_safe_remote_get(
$endpoint,
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => "Basic $api_key",
],
]
);

$response = self::request( 'GET', '', [], $api_key );
if ( is_wp_error( $response ) ) {
return $response;
}
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$parsed_response = json_decode( $response['body'], true );
return \rest_ensure_response( [ 'username' => $response['username'] ] );
}

/**
* Perform Mailchimp API Request
*
* @param string $method HTTP method.
* @param string $path API path.
* @param array $data Data to send.
* @param string $api_key Optional API key to override the default one.
*
* @return array|WP_Error response body or error.
*/
public static function request( $method, $path = '', $data = [], $api_key = '' ) {
if ( empty( $api_key ) ) {
// 'newspack_mailchimp_api_key' is a new option introduced to manage MC API key accross Newspack plugins.
// Keeping the old option for backwards compatibility.
$api_key = \get_option( 'newspack_mailchimp_api_key', get_option( 'newspack_newsletters_mailchimp_api_key' ) );
}
$endpoint = self::get_api_endpoint_from_key( $api_key );
if ( ! $endpoint ) {
return new \WP_Error( 'wrong_parameter', __( 'Invalid Mailchimp API Key.', 'newspack' ) );
}
$url = $endpoint . '/' . $path;
$config = [
'method' => $method,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => "Basic $api_key",
],
];
if ( ! empty( $data ) ) {
$config['body'] = \wp_json_encode( $data );
}
$response = \wp_safe_remote_request( $url, $config );

if ( \is_wp_error( $response ) ) {
return $response;
}
$parsed_response = json_decode( $response['body'], true );
if ( 200 !== \wp_remote_retrieve_response_code( $response ) ) {
return new \WP_Error(
'newspack_mailchimp_api',
array_key_exists( 'title', $parsed_response ) ? $parsed_response['title'] : __( 'Request failed.', 'newspack' )
);
}
return $parsed_response;
}

/**
* Perform a GET request to Mailchimp's API
*
* @param string $path API path.
*
* @return array|WP_Error API response or error.
*/
public static function get( $path = '' ) {
return self::request( 'GET', $path );
}

$response_body = json_decode( $response['body'], true );
return \rest_ensure_response( [ 'username' => $response_body['username'] ] );
/**
* Perform a PUT request to Mailchimp's API
*
* @param string $path API path.
* @param array $data Data to send.
*
* @return array|WP_Error API response or error.
*/
public static function put( $path = '', $data = [] ) {
return self::request( 'PUT', $path, $data );
}

/**
* Perform a POST request to Mailchimp's API
*
* @param string $path API path.
* @param array $data Data to send.
*
* @return array|WP_Error API response or error.
*/
public static function post( $path = '', $data = [] ) {
return self::request( 'POST', $path, $data );
}
}

Expand Down
1 change: 1 addition & 0 deletions includes/reader-activation/class-reader-activation.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ private static function get_settings_config() {
'sync_esp' => true,
'sync_esp_delete' => true,
'active_campaign_master_list' => '',
'mailchimp_audience_id' => '',
'emails' => Emails::get_emails( array_values( Reader_Activation_Emails::EMAIL_TYPES ), false ),
'sender_name' => Emails::get_from_name(),
'sender_email_address' => Emails::get_from_email(),
Expand Down
Loading