Skip to content

Instantly share code, notes, and snippets.

@goranefbl
Created September 4, 2025 18:06
Show Gist options
  • Select an option

  • Save goranefbl/48af0dbadab6c18aff36a640bfc4d114 to your computer and use it in GitHub Desktop.

Select an option

Save goranefbl/48af0dbadab6c18aff36a640bfc4d114 to your computer and use it in GitHub Desktop.
Klaviyo
<?php
/**
* Klaviyo Integration
*/
class WPGL_Klaviyo
{
private static $instance = null;
const INTEGRATION_KEY = 'klaviyo';
public static function init()
{
if (!self::is_active()) {
return;
}
if (self::$instance === null) {
self::$instance = new self();
}
// Register hooks
add_action('wpgens_loyalty_points_updated', [__CLASS__, 'update_klaviyo_loyalty_points'], 10, 5);
}
private static function is_active()
{
$settings = self::get_settings();
return isset($settings['enabled']) && $settings['enabled'] === '1' && !empty($settings['api_key']);
}
public static function get_settings()
{
$integrations = get_option('wpgens_loyalty_integrations', '{}');
$settings = json_decode($integrations, true);
$default_settings = [
'enabled' => '0',
'api_key' => '',
];
return isset($settings[self::INTEGRATION_KEY])
? wp_parse_args($settings[self::INTEGRATION_KEY], $default_settings)
: $default_settings;
}
public static function save_settings($settings)
{
// Sanitize settings
$sanitized_settings = [
'enabled' => isset($settings['enabled']) ? sanitize_text_field($settings['enabled']) : '0',
'api_key' => isset($settings['api_key']) ? sanitize_text_field($settings['api_key']) : '',
];
// Validate enabled is only 0 or 1
$sanitized_settings['enabled'] = in_array($sanitized_settings['enabled'], ['0', '1']) ? $sanitized_settings['enabled'] : '0';
// Validate API key format (should be non-empty when enabled)
if ($sanitized_settings['enabled'] === '1' && empty($sanitized_settings['api_key'])) {
return false;
}
$integrations = get_option('wpgens_loyalty_integrations', '{}');
$all_settings = json_decode($integrations, true) ?: [];
$all_settings[self::INTEGRATION_KEY] = wp_parse_args($sanitized_settings, self::get_settings());
return update_option('wpgens_loyalty_integrations', wp_json_encode($all_settings));
}
/**
* Map internal activity type/source to user-facing reason title
*
* @param string $type One of WPGL_Points_Activity_Type::*
* @param string $source One of WPGL_Points_Source_Type::*
* @return string
*/
private static function map_reason_title($type, $source)
{
// Normalize inputs for safety
$type = is_string($type) ? strtoupper($type) : '';
$source = is_string($source) ? strtoupper($source) : '';
// Expiration has priority regardless of source
if ($type === WPGL_Points_Activity_Type::EXPIRE || $source === WPGL_Points_Source_Type::POINTS_EXPIRED) {
return 'Points expired';
}
switch ($source) {
case WPGL_Points_Source_Type::PLACE_ORDER:
return 'Order purchase';
case WPGL_Points_Source_Type::ORDER_DISCOUNT:
return 'Order discount';
case WPGL_Points_Source_Type::ORDER_CANCELLED:
return 'Order cancelled';
case WPGL_Points_Source_Type::ORDER_REFUNDED:
return 'Order refunded';
case WPGL_Points_Source_Type::PRODUCT_REVIEW:
return 'Product review';
case WPGL_Points_Source_Type::REFER_FRIEND:
return 'Friend referral';
case WPGL_Points_Source_Type::MANUAL:
return 'Manual adjustment';
case WPGL_Points_Source_Type::REGISTRATION:
return 'Registration bonus';
case WPGL_Points_Source_Type::BIRTHDAY:
return 'Birthday bonus';
case WPGL_Points_Source_Type::IMPORT:
return 'Imported points';
default:
// Fallback to Title Case of $type
return ucfirst(strtolower($type));
}
}
/**
* Compute expiry date (ISO8601) if expiration is enabled.
* - For EXPIRE events, returns the awarded_at timestamp.
* - Otherwise, calculates last_activity + expiration_period days.
*
* @param int $user_id
* @param string $type
* @param string $awarded_at_iso
* @return string|null ISO8601 or null if not applicable
*/
private static function compute_expires_at($user_id, $type, $awarded_at_iso)
{
if (!class_exists('WPGL_Points_Expiry') || !WPGL_Points_Expiry::is_expiration_enabled()) {
return null;
}
// If this is an expiration event, use the event time
if ($type === WPGL_Points_Activity_Type::EXPIRE) {
return $awarded_at_iso;
}
$days = (int) WPGL_Points_Expiry::get_expiration_period();
if ($days <= 0) {
return null;
}
$last_activity = get_user_meta($user_id, WPGL_Points_Expiry::LAST_ACTIVITY_META_KEY, true);
if (empty($last_activity)) {
$base_ts = time();
} else {
$base_ts = strtotime($last_activity);
if ($base_ts === false) {
$base_ts = time();
}
}
$expires_ts = $base_ts + ($days * DAY_IN_SECONDS);
return gmdate('c', $expires_ts);
}
public static function update_klaviyo_loyalty_points($user_id, $new_balance, $points_delta, $type, $source)
{
// Skip no-op updates
if ((int)$points_delta === 0) return;
// 1. Get user email and name
$user = get_userdata($user_id);
if (!$user || !$user->user_email) return;
// 2. Get Klaviyo API settings
$settings = self::get_settings();
$klaviyo_api_key = $settings['api_key'];
$endpoint = 'https://a.klaviyo.com/api/profile-import';
// 3. Prepare profile data with names and minimal properties
$profile_data = [
'email' => $user->user_email,
'properties' => [
'loyalty_points_balance' => (int)$new_balance,
'loyalty_points_last_updated' => gmdate('c')
]
];
// Add first name if available
if (!empty($user->first_name)) {
$profile_data['first_name'] = $user->first_name;
}
// Add last name if available
if (!empty($user->last_name)) {
$profile_data['last_name'] = $user->last_name;
}
// 4. Create the full API request data
$data = [
'data' => [
'type' => 'profile',
'attributes' => $profile_data
]
];
// 5. Configure headers
$args = [
'headers' => [
'Authorization' => 'Klaviyo-API-Key ' . $klaviyo_api_key,
'Content-Type' => 'application/vnd.api+json',
'Accept' => 'application/vnd.api+json',
'revision' => '2024-05-15' // Latest revision
],
'body' => wp_json_encode($data),
'timeout' => 15 // Prevent long delays
];
// 6. Execute API call (profile import)
$response = wp_remote_post($endpoint, $args);
// 7. Error handling (profile)
if (is_wp_error($response)) {
WPGL_Logger::debug('Klaviyo connection error (profile): ' . $response->get_error_message());
} else {
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code < 200 || $status_code >= 300) {
$body = wp_remote_retrieve_body($response);
WPGL_Logger::debug("Klaviyo API Error (profile) ($status_code)", ['response' => $body]);
}
}
// 8. Emit event: "Points Updated"
$awarded_at_iso = gmdate('c');
$reason_title = self::map_reason_title($type, $source);
$expires_at_iso = self::compute_expires_at($user_id, $type, $awarded_at_iso);
$unique_id = hash('sha256', implode('|', [
strtolower($user->user_email),
(string) $type,
(string) $source,
(int) $points_delta,
(int) $new_balance
]));
$event_payload = [
'data' => [
'type' => 'event',
'attributes' => [
'time' => $awarded_at_iso,
'value' => (int) $points_delta,
'unique_id' => $unique_id,
'properties' => array_filter([
'delta' => (int) $points_delta,
'balance_after' => (int) $new_balance,
'reason_title' => $reason_title,
'source' => $source,
'awarded_at' => $awarded_at_iso,
'expires_at' => $expires_at_iso,
'unique_id' => $unique_id,
]),
'metric' => [
'data' => [
'type' => 'metric',
'attributes' => [
'name' => 'Points Updated',
],
],
],
'profile' => [
'data' => [
'type' => 'profile',
'attributes' => [
'email' => $user->user_email,
],
],
],
],
],
];
$events_endpoint = 'https://a.klaviyo.com/api/events/';
$event_args = [
'headers' => [
'Authorization' => 'Klaviyo-API-Key ' . $klaviyo_api_key,
'Content-Type' => 'application/vnd.api+json',
'Accept' => 'application/vnd.api+json',
'revision' => '2024-05-15',
],
'body' => wp_json_encode($event_payload),
'timeout' => 15,
];
$event_response = wp_remote_post($events_endpoint, $event_args);
if (is_wp_error($event_response)) {
WPGL_Logger::debug('Klaviyo connection error (event): ' . $event_response->get_error_message());
} else {
$event_status = wp_remote_retrieve_response_code($event_response);
if ($event_status < 200 || $event_status >= 300) {
$event_body = wp_remote_retrieve_body($event_response);
WPGL_Logger::debug("Klaviyo API Error (event) ($event_status)", ['response' => $event_body]);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment