From b500c35e53f37af861a1495047d4bd91d3216c54 Mon Sep 17 00:00:00 2001 From: rockwellll Date: Sun, 18 Jan 2026 12:31:34 +0300 Subject: [PATCH 01/13] add a constants.php file --- hellotext.php | 14 ++++++--- src/Api/Client.php | 6 ++-- src/Api/Event.php | 10 +++--- src/Api/Webchat.php | 5 +-- src/Constants.php | 43 +++++++++++++++++++++++++ src/Events/AppInstalled.php | 19 +++++------ src/Events/AppRemoved.php | 5 +-- src/Events/CartUpdates.php | 18 ++++++++--- src/Events/CouponRedeemed.php | 3 +- src/Events/CustomOptionsUpdated.php | 4 ++- src/Events/OrderPlaced.php | 9 +++--- src/Events/OrderStatus.php | 9 +++--- src/Events/ProductViewed.php | 3 +- src/Events/RefundReceived.php | 5 +-- src/Misc/Settings.php | 49 +++++++++++++++++------------ src/Services/CreateProfile.php | 25 ++++++++------- src/Services/Session.php | 13 ++++---- 17 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 src/Constants.php diff --git a/hellotext.php b/hellotext.php index 077a2cd..b5720aa 100644 --- a/hellotext.php +++ b/hellotext.php @@ -17,6 +17,10 @@ * Domain Path: /languages */ +use Hellotext\Constants; + +require_once plugin_dir_path(__FILE__) . 'src/Constants.php'; + // TODO: Refactor this to use the APP_ENV variable if (! isset($_ENV['APP_ENV'])) { $_ENV['APP_ENV'] = 'production'; @@ -89,11 +93,11 @@ function hellotext_load_textdomain() { function uninstall() { global $wpdb; - delete_option('hellotext_business_id'); - delete_option('hellotext_webchat_id'); - delete_option('hellotext_webchat_placement'); - delete_option('hellotext_webchat_behaviour'); - delete_option('hellotext_access_token'); + delete_option(Constants::OPTION_BUSINESS_ID); + delete_option(Constants::OPTION_WEBCHAT_ID); + delete_option(Constants::OPTION_WEBCHAT_PLACEMENT); + delete_option(Constants::OPTION_WEBCHAT_BEHAVIOUR); + delete_option(Constants::OPTION_ACCESS_TOKEN); $api_keys_table = $wpdb->prefix . 'woocommerce_api_keys'; if ($wpdb->get_var("SHOW TABLES LIKE '$api_keys_table'") === $api_keys_table) { diff --git a/src/Api/Client.php b/src/Api/Client.php index cf1434b..d008cfe 100644 --- a/src/Api/Client.php +++ b/src/Api/Client.php @@ -2,8 +2,10 @@ namespace Hellotext\Api; +use Hellotext\Constants; + class Client { - public static $sufix = '/v1'; + public static $sufix = '/' . Constants::API_VERSION; public static function with_sufix ($sufix = '') { if (0 < strlen($sufix) && '/' !== $sufix[0]) { @@ -62,7 +64,7 @@ private static function get_api_url () { } private static function set_curl_options ($curl, $method) { - $hellotext_access_token = get_option('hellotext_access_token'); + $hellotext_access_token = get_option(Constants::OPTION_ACCESS_TOKEN); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); diff --git a/src/Api/Event.php b/src/Api/Event.php index 99492ad..ef33fe6 100644 --- a/src/Api/Event.php +++ b/src/Api/Event.php @@ -2,9 +2,11 @@ namespace Hellotext\Api; +use Hellotext\Constants; + class Event { public function __construct ($session = null) { - $this->hellotext_business_id = get_option('hellotext_business_id'); + $this->hellotext_business_id = get_option(Constants::OPTION_BUSINESS_ID); $this->session = $session; $this->curl = curl_init($this->get_api_url()); $this->set_curl_options(); @@ -42,12 +44,12 @@ private function set_curl_options () { private function get_api_url () { global $HELLOTEXT_API_URL; - return $HELLOTEXT_API_URL . '/v1/track/events'; + return $HELLOTEXT_API_URL . Constants::API_ENDPOINT_TRACK; } private function browser_session () { - if (isset($_COOKIE['hello_session'])) { - return sanitize_text_field($_COOKIE['hello_session']); + if (isset($_COOKIE[Constants::SESSION_COOKIE_NAME])) { + return sanitize_text_field($_COOKIE[Constants::SESSION_COOKIE_NAME]); } return null; diff --git a/src/Api/Webchat.php b/src/Api/Webchat.php index eeda2e8..c5767a5 100644 --- a/src/Api/Webchat.php +++ b/src/Api/Webchat.php @@ -3,16 +3,17 @@ namespace Hellotext\Api; use Hellotext\Api\Client; +use Hellotext\Constants; class Webchat { public static function index() { - $hellotext_access_token = get_option('hellotext_access_token'); + $hellotext_access_token = get_option(Constants::OPTION_ACCESS_TOKEN); if(!$hellotext_access_token) { return []; } - $body = Client::with_sufix()->get('/v1/wordpress/webchats'); + $body = Client::with_sufix()->get(Constants::API_ENDPOINT_WEBCHATS); return is_array($body['body']) && isset($body['body']['ids']) ? $body['body']['ids'] : []; } } diff --git a/src/Constants.php b/src/Constants.php new file mode 100644 index 0000000..9c34446 --- /dev/null +++ b/src/Constants.php @@ -0,0 +1,43 @@ +post('/integrations/woo', [ + ->post(Constants::API_ENDPOINT_INTEGRATIONS_WOO, [ 'shop' => [ 'name' => get_bloginfo('name'), 'url' => get_bloginfo('url'), @@ -72,14 +73,14 @@ function maybe_trigger_integration($business_id) { return; } - $hellotext_access_token = get_option('hellotext_access_token'); + $hellotext_access_token = get_option(Constants::OPTION_ACCESS_TOKEN); if ($hellotext_access_token && $business_id) { set_transient('hellotext_integration_triggered', true, 10); do_action('hellotext_create_integration'); } else { add_action('shutdown', function () { $integration_flag = get_transient('hellotext_integration_triggered'); - $hellotext_access_token = get_option('hellotext_access_token'); + $hellotext_access_token = get_option(Constants::OPTION_ACCESS_TOKEN); if ($hellotext_access_token && !$integration_flag) { set_transient('hellotext_integration_triggered', true, 10); // Expires in 10 seconds. @@ -89,7 +90,7 @@ function maybe_trigger_integration($business_id) { } } -add_action('add_option_hellotext_business_id', 'after_business_id_set', 10, 1); -add_action('add_option_hellotext_access_token', 'maybe_trigger_integration', 10, 3); -add_action('update_option_hellotext_business_id', 'after_business_id_save', 10, 3); -add_action('update_option_hellotext_access_token', 'maybe_trigger_integration', 10, 4); +add_action('add_option_' . Constants::OPTION_BUSINESS_ID, 'after_business_id_set', 10, 1); +add_action('add_option_' . Constants::OPTION_ACCESS_TOKEN, 'maybe_trigger_integration', 10, 3); +add_action('update_option_' . Constants::OPTION_BUSINESS_ID, 'after_business_id_save', 10, 3); +add_action('update_option_' . Constants::OPTION_ACCESS_TOKEN, 'maybe_trigger_integration', 10, 4); diff --git a/src/Events/AppRemoved.php b/src/Events/AppRemoved.php index 37c3b56..f7a4c71 100644 --- a/src/Events/AppRemoved.php +++ b/src/Events/AppRemoved.php @@ -2,10 +2,11 @@ use Hellotext\Api\Client; use Hellotext\Api\Event; +use Hellotext\Constants; function hellotext_deactivate ($hellotext_business_id = null) { if (!$hellotext_business_id) { - $hellotext_business_id = get_option('hellotext_business_id'); + $hellotext_business_id = get_option(Constants::OPTION_BUSINESS_ID); } if (!$hellotext_business_id) { @@ -17,7 +18,7 @@ function hellotext_deactivate ($hellotext_business_id = null) { add_action('hellotext_remove_integration', function ($business_id) { Client::with_sufix() - ->delete('/integrations/woo', [ + ->delete(Constants::API_ENDPOINT_INTEGRATIONS_WOO, [ 'shop' => [ 'business_id' => $business_id, ] diff --git a/src/Events/CartUpdates.php b/src/Events/CartUpdates.php index d56e59b..0ac3fea 100644 --- a/src/Events/CartUpdates.php +++ b/src/Events/CartUpdates.php @@ -2,6 +2,7 @@ use Hellotext\Adapters\ProductAdapter; use Hellotext\Api\Event; +use Hellotext\Constants; // We could listen for woocommerce_cart_updated event but this event is // triggered too many times per cart update. Instead, we listen for the @@ -27,8 +28,8 @@ function hellotext_cart_updated() { ); // Set previous cart items and current cart items - $previous_cart_items = isset($_SESSION['hellotext_cart_items']) - ? json_decode(sanitize_text_field($_SESSION['hellotext_cart_items']), true) + $previous_cart_items = isset($_SESSION[Constants::SESSION_CART_ITEMS]) + ? json_decode(sanitize_text_field($_SESSION[Constants::SESSION_CART_ITEMS]), true) : array(); $current_cart_items = WC()->cart->get_cart(); @@ -44,7 +45,7 @@ function hellotext_cart_updated() { } // Save current cart items to session - $_SESSION['hellotext_cart_items'] = json_encode($cart_items); + $_SESSION[Constants::SESSION_CART_ITEMS] = json_encode($cart_items); // Calculate total cart value $cart_total = WC()->cart->get_cart_contents_total(); @@ -81,11 +82,20 @@ function hellotext_cart_updated() { } // Trigger events, one for added and one for removed items + $event_map = [ + 'added' => Constants::EVENT_CART_ADDED, + 'removed' => Constants::EVENT_CART_REMOVED, + ]; + foreach ($changes as $event => $items) { if (0 == count($items)) { continue; } + if (!isset($event_map[$event])) { + continue; + } + $event_data = array( 'amount' => $cart_total, 'currency' => $currency, @@ -95,6 +105,6 @@ function hellotext_cart_updated() { ) ); - (new Event())->track("cart.{$event}", $event_data); + (new Event())->track($event_map[$event], $event_data); } } diff --git a/src/Events/CouponRedeemed.php b/src/Events/CouponRedeemed.php index 47afe87..8c3717d 100644 --- a/src/Events/CouponRedeemed.php +++ b/src/Events/CouponRedeemed.php @@ -1,6 +1,7 @@ track('coupon.redeemed', [ + ( new Event() )->track(Constants::EVENT_COUPON_REDEEMED, [ 'object_parameters' => [ 'type' => 'coupon', 'reference' => $coupon->get_id(), diff --git a/src/Events/CustomOptionsUpdated.php b/src/Events/CustomOptionsUpdated.php index 02cdffc..dc062af 100644 --- a/src/Events/CustomOptionsUpdated.php +++ b/src/Events/CustomOptionsUpdated.php @@ -1,8 +1,10 @@ get(); - $session = isset($_COOKIE['hello_session']) - ? sanitize_text_field($_COOKIE['hello_session']) + $session = isset($_COOKIE[Constants::SESSION_COOKIE_NAME]) + ? sanitize_text_field($_COOKIE[Constants::SESSION_COOKIE_NAME]) : null; $encrypted_session = Session::encrypt($session); - add_post_meta($order->get_id(), 'hellotext_session', $encrypted_session); + add_post_meta($order->get_id(), Constants::META_SESSION, $encrypted_session); - $event->track('order.placed', array( + $event->track(Constants::EVENT_ORDER_PLACED, array( 'object_parameters' => $parsedOrder, )); } diff --git a/src/Events/OrderStatus.php b/src/Events/OrderStatus.php index 13f975a..c51ad6e 100644 --- a/src/Events/OrderStatus.php +++ b/src/Events/OrderStatus.php @@ -3,11 +3,12 @@ use Hellotext\Adapters\OrderAdapter; use Hellotext\Services\Session; use Hellotext\Api\Event; +use Hellotext\Constants; add_action('woocommerce_order_status_changed', 'track_order_status', 10, 4); function track_order_status ($order_id, $old_status, $new_status, $order) { - $encrypted_session = get_post_meta($order_id, 'hellotext_session', true); + $encrypted_session = get_post_meta($order_id, Constants::META_SESSION, true); $session = Session::decrypt($encrypted_session); $orderAdapter = new OrderAdapter($order); @@ -17,19 +18,19 @@ function track_order_status ($order_id, $old_status, $new_status, $order) { switch ($new_status) { case 'processing': - $event->track('order.confirmed', array( + $event->track(Constants::EVENT_ORDER_CONFIRMED, array( 'object_parameters' => $orderAdapter->get(), )); break; case 'cancelled': - $event->track('order.cancelled', array( + $event->track(Constants::EVENT_ORDER_CANCELLED, array( 'object_parameters' => $orderAdapter->get(), )); break; case 'completed': - $event->track('order.delivered', array( + $event->track(Constants::EVENT_ORDER_DELIVERED, array( 'object_parameters' => $orderAdapter->get(), )); break; diff --git a/src/Events/ProductViewed.php b/src/Events/ProductViewed.php index ac4cf5c..61f0dfb 100644 --- a/src/Events/ProductViewed.php +++ b/src/Events/ProductViewed.php @@ -2,6 +2,7 @@ use Hellotext\Adapters\ProductAdapter; use Hellotext\Api\Event; +use Hellotext\Constants; add_action('woocommerce_after_single_product', 'hellotext_product_viewed'); @@ -10,7 +11,7 @@ function hellotext_product_viewed() { do_action('hellotext_create_profile'); - ( new Event() )->track('product.viewed', array( + ( new Event() )->track(Constants::EVENT_PRODUCT_VIEWED, array( 'object_parameters' => ( new ProductAdapter($product) )->get() )); } diff --git a/src/Events/RefundReceived.php b/src/Events/RefundReceived.php index 116b0c5..d4b908b 100644 --- a/src/Events/RefundReceived.php +++ b/src/Events/RefundReceived.php @@ -3,6 +3,7 @@ use Hellotext\Adapters\RefundAdapter; use Hellotext\Services\Session; use Hellotext\Api\Event; +use Hellotext\Constants; add_action( 'woocommerce_order_refunded', 'hellotext_refund_created', 10, 2 ); @@ -12,10 +13,10 @@ function hellotext_refund_created ($order_id, $refund_id) { do_action('hellotext_create_profile', $order->get_user_id()); - $encrypted_session = get_post_meta($order_id, 'hellotext_session', true); + $encrypted_session = get_post_meta($order_id, Constants::META_SESSION, true); $session = Session::decrypt($encrypted_session); - ( new Event($session) )->track('refund.received', array( + ( new Event($session) )->track(Constants::EVENT_REFUND_RECEIVED, array( 'object_parameters' => ( new RefundAdapter($refund, $order) )->get(), )); } diff --git a/src/Misc/Settings.php b/src/Misc/Settings.php index fa99a3d..1bcda5a 100644 --- a/src/Misc/Settings.php +++ b/src/Misc/Settings.php @@ -1,6 +1,7 @@ ' . wp_kses( __( 'description.paragraphs.one', 'hellotext' ), array( 'a' => array( 'href' => array(), 'target' => array(), 'style' => array() ) ) ) . '

'; @@ -79,22 +80,24 @@ function hellotext_description_section_callback() { function hellotext_business_id_field() { ?> - - + ' . __('webchat_unavailable', 'hellotext') . '

'; @@ -102,7 +105,9 @@ function hellotext_webchat_id_field() { } ?> - @@ -115,10 +120,12 @@ function hellotext_webchat_id_field() { } function hellotext_webchat_placement_field() { - $placement = get_option('hellotext_webchat_placement', 'bottom-right'); // 'bottom-right' is the default value + $placement = get_option(Constants::OPTION_WEBCHAT_PLACEMENT, 'bottom-right'); // 'bottom-right' is the default value ?> - @@ -136,10 +143,12 @@ function hellotext_webchat_placement_field() { } function hellotext_webchat_behaviour_field() { - $behaviour = get_option('hellotext_webchat_behaviour', 'popover'); // 'popover' is the default value + $behaviour = get_option(Constants::OPTION_WEBCHAT_BEHAVIOUR, 'popover'); // 'popover' is the default value ?> - diff --git a/src/Services/CreateProfile.php b/src/Services/CreateProfile.php index 79861df..1881d9e 100644 --- a/src/Services/CreateProfile.php +++ b/src/Services/CreateProfile.php @@ -3,6 +3,7 @@ namespace Hellotext\Services; use Hellotext\Api\Client; +use Hellotext\Constants; class CreateProfile { public $user; @@ -16,7 +17,9 @@ class CreateProfile { public function __construct($user_id, $billing = []) { $this->user_id = $user_id; - $this->session = isset($_COOKIE['hello_session']) ? sanitize_text_field($_COOKIE['hello_session']) : null; + $this->session = isset($_COOKIE[Constants::SESSION_COOKIE_NAME]) + ? sanitize_text_field($_COOKIE[Constants::SESSION_COOKIE_NAME]) + : null; $this->client = Client::class; $this->billing = $billing; } @@ -52,24 +55,24 @@ private function get_user () { } private function verify_if_profile_exists () { - $hellotext_profile_id = get_user_meta($this->user_id ?? $this->session, 'hellotext_profile_id', true); + $hellotext_profile_id = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); return false != $hellotext_profile_id && '' != $hellotext_profile_id; } private function session_changed () { - return get_user_meta($this->user_id, 'hellotext_session', true) != $this->session; + return get_user_meta($this->user_id, Constants::META_SESSION, true) != $this->session; } public function create_hellotext_profile () { - $profile = get_user_meta($this->user_id ?? $this->session, 'hellotext_profile_id', true); + $profile = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); if ($profile) { if (isset($this->user)) { - update_user_meta($this->user->ID, 'hellotext_profile_id', $profile); + update_user_meta($this->user->ID, Constants::META_PROFILE_ID, $profile); } - $this->client::patch("/sessions/{$this->session}", array( + $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", array( 'session' => $this->session, 'profile' => $profile, )); @@ -79,7 +82,7 @@ public function create_hellotext_profile () { $phone = get_user_meta($this->user_id, 'billing_phone', true); - $response = $this->client::post('/profiles', array_filter(array( + $response = $this->client::post(Constants::API_ENDPOINT_PROFILES, array_filter(array( 'session' => $this->session, 'reference' => isset($this->user) ? $this->user->ID : null, 'first_name' => $this->user->nickname ?? $this->billing['first_name'], @@ -89,18 +92,18 @@ public function create_hellotext_profile () { 'lists' => array('WooCommerce'), ))); - add_user_meta( $this->user_id ?? $this->session, 'hellotext_profile_id', $response['body']['id'], true ); + add_user_meta( $this->user_id ?? $this->session, Constants::META_PROFILE_ID, $response['body']['id'], true ); - $this->client::patch("/sessions/{$this->session}", array( + $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", array( 'session' => $this->session, 'profile' => $response['body']['id'], )); } private function attach_profile_to_session () { - $profile_id = get_user_meta($this->user_id ?? $this->session, 'hellotext_profile_id', true); + $profile_id = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); - $response = $this->client::patch("/sessions/{$this->session}", array( + $response = $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", array( 'session' => $this->session, 'profile' => $profile_id, )); diff --git a/src/Services/Session.php b/src/Services/Session.php index 5312294..16be623 100644 --- a/src/Services/Session.php +++ b/src/Services/Session.php @@ -2,25 +2,26 @@ namespace Hellotext\Services; +use Hellotext\Constants; + class Session { - const METHOD = 'aes-256-cbc'; public static function encrypt ($session = null) { - $key = get_option('hellotext_business_id'); + $key = get_option(Constants::OPTION_BUSINESS_ID); // Generate an initialization vector (IV) - $iv_length = openssl_cipher_iv_length(self::METHOD); + $iv_length = openssl_cipher_iv_length(Constants::ENCRYPTION_METHOD); $iv = openssl_random_pseudo_bytes($iv_length); - $encrypted = openssl_encrypt($session, self::METHOD, $key, 0, $iv); + $encrypted = openssl_encrypt($session, Constants::ENCRYPTION_METHOD, $key, 0, $iv); return base64_encode($encrypted . '::' . $iv); } public static function decrypt ($encrypted_data = null) { - $key = get_option('hellotext_business_id'); + $key = get_option(Constants::OPTION_BUSINESS_ID); $parts = explode('::', base64_decode($encrypted_data)); - return openssl_decrypt($parts[0], self::METHOD, $key, 0, $parts[1]); + return openssl_decrypt($parts[0], Constants::ENCRYPTION_METHOD, $key, 0, $parts[1]); } } From 141448947b9dc3d681b56710dca04c7b0ef1d3ba Mon Sep 17 00:00:00 2001 From: rockwellll Date: Sun, 18 Jan 2026 12:37:19 +0300 Subject: [PATCH 02/13] add typehints to method signatures --- src/Adapters/OrderAdapter.php | 10 +++++----- src/Adapters/PriceAdapter.php | 8 ++++---- src/Adapters/ProductAdapter.php | 6 +++--- src/Adapters/RefundAdapter.php | 7 ++++--- src/Api/Client.php | 20 ++++++++++---------- src/Api/Event.php | 14 +++++++++----- src/Api/Webchat.php | 2 +- src/Events/AppInstalled.php | 12 ++++++------ src/Events/AppRemoved.php | 4 ++-- src/Events/CartUpdates.php | 4 ++-- src/Events/CouponRedeemed.php | 2 +- src/Events/CustomOptionsUpdated.php | 2 +- src/Events/OrderPlaced.php | 2 +- src/Events/OrderStatus.php | 2 +- src/Events/ProductViewed.php | 2 +- src/Events/RefundReceived.php | 2 +- src/Events/UserRegistered.php | 2 +- src/Misc/Scripts.php | 2 +- src/Misc/Settings.php | 20 ++++++++++---------- src/Services/CreateProfile.php | 28 ++++++++++++++-------------- src/Services/Session.php | 4 ++-- 21 files changed, 80 insertions(+), 75 deletions(-) diff --git a/src/Adapters/OrderAdapter.php b/src/Adapters/OrderAdapter.php index 92047bc..c8cc42f 100644 --- a/src/Adapters/OrderAdapter.php +++ b/src/Adapters/OrderAdapter.php @@ -6,15 +6,15 @@ use Hellotext\Adapters\PriceAdapter; class OrderAdapter { - public $order; // WooCommerce Order - public $products; // Order items + public \WC_Order|false $order; // WooCommerce Order + public array $products; // Order items - public function __construct ($order, $products = []) { + public function __construct (int|\WC_Order $order, array $products = []) { $this->order = is_numeric($order) ? wc_get_order( $order ) : $order; $this->products = $products; } - public function get () { + public function get (): array { if (!$this->order) { throw new \Exception('Order not found'); } @@ -28,7 +28,7 @@ public function get () { ); } - public function adapted_products () { + public function adapted_products (): array { $items = $this->order->get_items(); foreach ($items as $item) { diff --git a/src/Adapters/PriceAdapter.php b/src/Adapters/PriceAdapter.php index efc9f05..b447390 100644 --- a/src/Adapters/PriceAdapter.php +++ b/src/Adapters/PriceAdapter.php @@ -3,15 +3,15 @@ namespace Hellotext\Adapters; class PriceAdapter { - public $price; - public $currency; + public float|string $price; + public string $currency; - public function __construct ($price, $currency = null) { + public function __construct (float|string $price, ?string $currency = null) { $this->price = $price; $this->currency = is_null($currency) ? get_woocommerce_currency() : $currency; } - public function get () { + public function get (): array { return array( 'amount' => $this->price, 'currency' => $this->currency, diff --git a/src/Adapters/ProductAdapter.php b/src/Adapters/ProductAdapter.php index d42d810..293701e 100644 --- a/src/Adapters/ProductAdapter.php +++ b/src/Adapters/ProductAdapter.php @@ -5,13 +5,13 @@ use Hellotext\Adapters\PriceAdapter; class ProductAdapter { - public $product; // WooCommerce product + public \WC_Product|false $product; // WooCommerce product - public function __construct ($product) { + public function __construct (int|\WC_Product $product) { $this->product = is_numeric($product) ? wc_get_product( $product ) : $product; } - public function get () { + public function get (): array { if (!$this->product) { throw new \Exception('Product not found'); } diff --git a/src/Adapters/RefundAdapter.php b/src/Adapters/RefundAdapter.php index 475c396..7707e86 100644 --- a/src/Adapters/RefundAdapter.php +++ b/src/Adapters/RefundAdapter.php @@ -5,14 +5,15 @@ use Hellotext\Adapters\PriceAdapter; class RefundAdapter { - public $refund; // WooCommerce Refund + public \WC_Order_Refund $refund; // WooCommerce Refund + public \WC_Order $order; - public function __construct ($refund, $order) { + public function __construct (\WC_Order_Refund $refund, \WC_Order $order) { $this->refund = $refund; $this->order = $order; } - public function get () { + public function get (): array { if (!$this->refund) { throw new \Exception('Refund not found'); } diff --git a/src/Api/Client.php b/src/Api/Client.php index d008cfe..76c7a2f 100644 --- a/src/Api/Client.php +++ b/src/Api/Client.php @@ -5,9 +5,9 @@ use Hellotext\Constants; class Client { - public static $sufix = '/' . Constants::API_VERSION; + public static string $sufix = '/' . Constants::API_VERSION; - public static function with_sufix ($sufix = '') { + public static function with_sufix (string $sufix = ''): self { if (0 < strlen($sufix) && '/' !== $sufix[0]) { $sufix = '/' . $sufix; } @@ -17,7 +17,7 @@ public static function with_sufix ($sufix = '') { return new self(); } - public static function request ($method = 'GET', $path = '/', $data = []) { + public static function request (string $method = 'GET', string $path = '/', array $data = []): array { $request_url = self::get_api_url() . $path; $curl = curl_init($request_url); self::set_curl_options($curl, $method); @@ -37,33 +37,33 @@ public static function request ($method = 'GET', $path = '/', $data = []) { ); } - public static function get ($path = '/', $data = null) { + public static function get (string $path = '/', ?array $data = null): array { return self::request('GET', $path, $data); } - public static function post ($path = '/', $data = null) { + public static function post (string $path = '/', ?array $data = null): array { return self::request('POST', $path, $data); } - public static function patch ($path = '/', $data = null) { + public static function patch (string $path = '/', ?array $data = null): array { return self::request('PATCH', $path, $data); } - public static function put ($path = '/', $data = null) { + public static function put (string $path = '/', ?array $data = null): array { return self::request('PUT', $path, $data); } - public static function delete ($path = '/', $data = null) { + public static function delete (string $path = '/', ?array $data = null): array { return self::request('DELETE', $path, $data); } - private static function get_api_url () { + private static function get_api_url (): string { global $HELLOTEXT_API_URL; return $HELLOTEXT_API_URL . self::$sufix; } - private static function set_curl_options ($curl, $method) { + private static function set_curl_options ($curl, string $method): void { $hellotext_access_token = get_option(Constants::OPTION_ACCESS_TOKEN); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); diff --git a/src/Api/Event.php b/src/Api/Event.php index ef33fe6..37521d9 100644 --- a/src/Api/Event.php +++ b/src/Api/Event.php @@ -5,14 +5,18 @@ use Hellotext\Constants; class Event { - public function __construct ($session = null) { + private string $hellotext_business_id; + private ?string $session; + private $curl; + + public function __construct (?string $session = null) { $this->hellotext_business_id = get_option(Constants::OPTION_BUSINESS_ID); $this->session = $session; $this->curl = curl_init($this->get_api_url()); $this->set_curl_options(); } - public function track ($action, $payload) { + public function track (string $action, array $payload): void { $body = array_merge( array( 'action' => $action, @@ -30,7 +34,7 @@ public function track ($action, $payload) { curl_close($this->curl); } - private function set_curl_options () { + private function set_curl_options (): void { curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST'); @@ -41,13 +45,13 @@ private function set_curl_options () { )); } - private function get_api_url () { + private function get_api_url (): string { global $HELLOTEXT_API_URL; return $HELLOTEXT_API_URL . Constants::API_ENDPOINT_TRACK; } - private function browser_session () { + private function browser_session (): ?string { if (isset($_COOKIE[Constants::SESSION_COOKIE_NAME])) { return sanitize_text_field($_COOKIE[Constants::SESSION_COOKIE_NAME]); } diff --git a/src/Api/Webchat.php b/src/Api/Webchat.php index c5767a5..e6d4cfb 100644 --- a/src/Api/Webchat.php +++ b/src/Api/Webchat.php @@ -6,7 +6,7 @@ use Hellotext\Constants; class Webchat { - public static function index() { + public static function index(): array { $hellotext_access_token = get_option(Constants::OPTION_ACCESS_TOKEN); if(!$hellotext_access_token) { diff --git a/src/Events/AppInstalled.php b/src/Events/AppInstalled.php index 1ff7d0e..7fa0985 100644 --- a/src/Events/AppInstalled.php +++ b/src/Events/AppInstalled.php @@ -4,7 +4,7 @@ use Hellotext\Api\Event; use Hellotext\Constants; -function hellotext_activate () { +function hellotext_activate (): void { $hellotext_business_id = get_option(Constants::OPTION_BUSINESS_ID); if (!$hellotext_business_id) { return; @@ -13,7 +13,7 @@ function hellotext_activate () { do_action('hellotext_create_integration', $hellotext_business_id); } -add_action('hellotext_create_integration', function ($business_id) { +add_action('hellotext_create_integration', function (mixed $business_id): void { if(!$business_id) { $business_id = get_option(Constants::OPTION_BUSINESS_ID); } @@ -56,17 +56,17 @@ function hellotext_activate () { } }); -function after_business_id_save($old_value, $new_value) { +function after_business_id_save(mixed $old_value, mixed $new_value): void { if ($old_value !== $new_value) { maybe_trigger_integration($new_value); } } -function after_business_id_set($value) { +function after_business_id_set(mixed $value): void { maybe_trigger_integration($value); } -function maybe_trigger_integration($business_id) { +function maybe_trigger_integration(mixed $business_id): void { $integration_flag = get_transient('hellotext_integration_triggered'); if ($integration_flag) { @@ -78,7 +78,7 @@ function maybe_trigger_integration($business_id) { set_transient('hellotext_integration_triggered', true, 10); do_action('hellotext_create_integration'); } else { - add_action('shutdown', function () { + add_action('shutdown', function (): void { $integration_flag = get_transient('hellotext_integration_triggered'); $hellotext_access_token = get_option(Constants::OPTION_ACCESS_TOKEN); diff --git a/src/Events/AppRemoved.php b/src/Events/AppRemoved.php index f7a4c71..49f0058 100644 --- a/src/Events/AppRemoved.php +++ b/src/Events/AppRemoved.php @@ -4,7 +4,7 @@ use Hellotext\Api\Event; use Hellotext\Constants; -function hellotext_deactivate ($hellotext_business_id = null) { +function hellotext_deactivate (?string $hellotext_business_id = null): void { if (!$hellotext_business_id) { $hellotext_business_id = get_option(Constants::OPTION_BUSINESS_ID); } @@ -16,7 +16,7 @@ function hellotext_deactivate ($hellotext_business_id = null) { do_action('hellotext_remove_integration', $hellotext_business_id); } -add_action('hellotext_remove_integration', function ($business_id) { +add_action('hellotext_remove_integration', function (mixed $business_id): void { Client::with_sufix() ->delete(Constants::API_ENDPOINT_INTEGRATIONS_WOO, [ 'shop' => [ diff --git a/src/Events/CartUpdates.php b/src/Events/CartUpdates.php index 0ac3fea..7549c9c 100644 --- a/src/Events/CartUpdates.php +++ b/src/Events/CartUpdates.php @@ -12,14 +12,14 @@ add_action( 'woocommerce_cart_item_removed', 'hellotext_trigger_cart_updated' ); add_action( 'woocommerce_after_cart_item_quantity_update', 'hellotext_trigger_cart_updated' ); -function hellotext_trigger_cart_updated () { +function hellotext_trigger_cart_updated (): void { do_action('hellotext_create_profile'); do_action('hellotext_woocommerce_cart_updated'); } add_action('hellotext_woocommerce_cart_updated', 'hellotext_cart_updated'); -function hellotext_cart_updated() { +function hellotext_cart_updated(): void { wc_load_cart(); $changes = array( diff --git a/src/Events/CouponRedeemed.php b/src/Events/CouponRedeemed.php index 8c3717d..5aa54c8 100644 --- a/src/Events/CouponRedeemed.php +++ b/src/Events/CouponRedeemed.php @@ -5,7 +5,7 @@ add_action( 'woocommerce_applied_coupon', 'hellotext_coupon_redeemed', 10, 1 ); -function hellotext_coupon_redeemed ($code) { +function hellotext_coupon_redeemed (string $code): void { do_action('hellotext_create_profile'); $coupon = new \WC_Coupon($code); diff --git a/src/Events/CustomOptionsUpdated.php b/src/Events/CustomOptionsUpdated.php index dc062af..7c9db0d 100644 --- a/src/Events/CustomOptionsUpdated.php +++ b/src/Events/CustomOptionsUpdated.php @@ -2,7 +2,7 @@ use Hellotext\Constants; -function custom_field_updated($option, $old_value, $new_value) { +function custom_field_updated(string $option, mixed $old_value, mixed $new_value): void { switch ($option) { case Constants::OPTION_BUSINESS_ID: do_action('hellotext_remove_integration', $old_value); diff --git a/src/Events/OrderPlaced.php b/src/Events/OrderPlaced.php index 6a06e2d..b29b5a3 100644 --- a/src/Events/OrderPlaced.php +++ b/src/Events/OrderPlaced.php @@ -7,7 +7,7 @@ add_action( 'woocommerce_after_order_details', 'hellotext_order_placed' ); -function hellotext_order_placed ( $order ) { +function hellotext_order_placed ( \WC_Order $order ): void { $userId = $order->get_user_id(); $userId = $userId > 0 ? $userId : $order->data['billing']; diff --git a/src/Events/OrderStatus.php b/src/Events/OrderStatus.php index c51ad6e..ac7d3b3 100644 --- a/src/Events/OrderStatus.php +++ b/src/Events/OrderStatus.php @@ -7,7 +7,7 @@ add_action('woocommerce_order_status_changed', 'track_order_status', 10, 4); -function track_order_status ($order_id, $old_status, $new_status, $order) { +function track_order_status (int $order_id, string $old_status, string $new_status, \WC_Order $order): void { $encrypted_session = get_post_meta($order_id, Constants::META_SESSION, true); $session = Session::decrypt($encrypted_session); diff --git a/src/Events/ProductViewed.php b/src/Events/ProductViewed.php index 61f0dfb..012bed6 100644 --- a/src/Events/ProductViewed.php +++ b/src/Events/ProductViewed.php @@ -6,7 +6,7 @@ add_action('woocommerce_after_single_product', 'hellotext_product_viewed'); -function hellotext_product_viewed() { +function hellotext_product_viewed(): void { global $product; do_action('hellotext_create_profile'); diff --git a/src/Events/RefundReceived.php b/src/Events/RefundReceived.php index d4b908b..6474bf1 100644 --- a/src/Events/RefundReceived.php +++ b/src/Events/RefundReceived.php @@ -7,7 +7,7 @@ add_action( 'woocommerce_order_refunded', 'hellotext_refund_created', 10, 2 ); -function hellotext_refund_created ($order_id, $refund_id) { +function hellotext_refund_created (int $order_id, int $refund_id): void { $order = wc_get_order($order_id); $refund = new WC_Order_Refund($refund_id); diff --git a/src/Events/UserRegistered.php b/src/Events/UserRegistered.php index e4e4932..9464324 100644 --- a/src/Events/UserRegistered.php +++ b/src/Events/UserRegistered.php @@ -4,7 +4,7 @@ add_action( 'user_register', 'hellotext_user_registered', 10, 1 ); -function hellotext_user_registered ($user_id) { +function hellotext_user_registered (int $user_id): void { $service = new CreateProfile($user_id); $service->process(); } diff --git a/src/Misc/Scripts.php b/src/Misc/Scripts.php index 3bde4a8..395a8bd 100644 --- a/src/Misc/Scripts.php +++ b/src/Misc/Scripts.php @@ -3,7 +3,7 @@ add_action( 'admin_head', 'hellotext_script' ); add_action( 'wp_head', 'hellotext_script' ); -function hellotext_script () { +function hellotext_script (): void { global $HELLOTEXT_API_URL; $business_id = get_option('hellotext_business_id'); diff --git a/src/Misc/Settings.php b/src/Misc/Settings.php index 1bcda5a..475dcf1 100644 --- a/src/Misc/Settings.php +++ b/src/Misc/Settings.php @@ -4,7 +4,7 @@ use Hellotext\Constants; add_action( 'admin_init', 'hellotext_settings_init' ); -function hellotext_settings_init() { +function hellotext_settings_init(): void { // Add settings section add_settings_section( @@ -65,7 +65,7 @@ function hellotext_settings_init() { } -function hellotext_description_section_callback() { +function hellotext_description_section_callback(): void { $business_id = get_option(Constants::OPTION_BUSINESS_ID, null); $access_token = get_option(Constants::OPTION_ACCESS_TOKEN, null); @@ -78,7 +78,7 @@ function hellotext_description_section_callback() { } } -function hellotext_business_id_field() { +function hellotext_business_id_field(): void { ?> + style="width: 400px;" rows="5"> " name="" style="width: 400px;"> - - - - " name="" style="width: 400px;"> - - + + +function init_hellotext(): void { + /** + * Register the WooCommerce submenu for Hellotext. + * + * @return void + */ + function custom_woocommerce_menu(): void { + add_submenu_page( + 'woocommerce', + 'Hellotext', + 'Hellotext', + 'manage_options', + 'wc-hellotext', + 'hellotext_submenu_page_callback' + ); + } + add_action('admin_menu', 'custom_woocommerce_menu'); + + /** + * Render the Hellotext settings page. + * + * @return void + */ + function hellotext_submenu_page_callback(): void { + ?>

hellotext @@ -230,28 +227,28 @@ function hellotext_submenu_page_callback (): void {

Important: Please select any Permalink structure other than "Plain" in Settings > Permalinks. Otherwise, the plugin will not work.
HTML; } - ?> + ?>
'background-color: #FF4C00; color: #FFFFFF; border: none;') - ); - ?> + settings_fields('hellotext-form'); + do_settings_sections('hellotext-form'); + submit_button( + __('settings.submit', 'hellotext'), + null, + null, + false, + ['style' => 'background-color: #FF4C00; color: #FFFFFF; border: none;'] + ); + ?>
user_id = $user_id; - $this->session = isset($_COOKIE[Constants::SESSION_COOKIE_NAME]) - ? sanitize_text_field($_COOKIE[Constants::SESSION_COOKIE_NAME]) - : null; - $this->client = Client::class; - $this->billing = $billing; - } - - /** - * Process profile creation/association flow. - * - * @return void - */ - public function process (): void { - if (isset($this->billing) && !empty($this->billing)) { - $this->create_hellotext_profile(); - $this->attach_profile_to_session(); - return; - } - - if (! $this->user_id) { - return; - } - - if (! $this->verify_if_profile_exists()) { - $this->get_user(); - $this->create_hellotext_profile(); - $this->attach_profile_to_session(); - } - - if ($this->session_changed()) { - $this->attach_profile_to_session(); - } - } - - /** - * Load the WordPress user by ID. - * - * @return void - * @throws \Exception When user is not found. - */ - private function get_user (): void { - $this->user = get_user_by('id', $this->user_id); - - if (!$this->user) { - throw new \Exception("User with id {$this->user_id} not found"); - } - } - - /** - * Check if a Hellotext profile already exists. - * - * @return bool - */ - private function verify_if_profile_exists (): bool { - $hellotext_profile_id = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); - - return false != $hellotext_profile_id && '' != $hellotext_profile_id; - } - - /** - * Determine if the session value has changed. - * - * @return bool - */ - private function session_changed (): bool { - return get_user_meta($this->user_id, Constants::META_SESSION, true) != $this->session; - } - - /** - * Create a Hellotext profile or reuse existing one. - * - * @return void - */ - public function create_hellotext_profile (): void { - $profile = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); - - if ($profile) { - if (isset($this->user)) { - update_user_meta($this->user->ID, Constants::META_PROFILE_ID, $profile); - } - - $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", array( + /** + * WordPress user instance. + * + * @var \WP_User|false|null + */ + public \WP_User|false|null $user = null; + + /** + * Hellotext profile ID. + * + * @var string|null + */ + public ?string $hellotext_profile_id = null; + + /** + * API client class name. + * + * @var string + */ + public string $client; + + /** + * Billing payload data. + * + * @var array + */ + public array $billing; + + /** + * WordPress user ID. + * + * @var int|null + */ + public ?int $user_id; + + /** + * Session identifier. + * + * @var string|null + */ + public ?string $session; + + /** + * Create a new profile service instance. + * + * @param int|null $user_id WordPress user ID. + * @param array $billing Billing payload data. + */ + public function __construct(?int $user_id, array $billing = []) { + $this->user_id = $user_id; + $this->session = isset($_COOKIE[Constants::SESSION_COOKIE_NAME]) + ? sanitize_text_field($_COOKIE[Constants::SESSION_COOKIE_NAME]) + : null; + $this->client = Client::class; + $this->billing = $billing; + } + + /** + * Process profile creation/association flow. + * + * @return void + */ + public function process(): void { + if (isset($this->billing) && !empty($this->billing)) { + $this->create_hellotext_profile(); + $this->attach_profile_to_session(); + return; + } + + if (! $this->user_id) { + return; + } + + if (! $this->verify_if_profile_exists()) { + $this->get_user(); + $this->create_hellotext_profile(); + $this->attach_profile_to_session(); + } + + if ($this->session_changed()) { + $this->attach_profile_to_session(); + } + } + + /** + * Load the WordPress user by ID. + * + * @return void + * @throws \Exception When user is not found. + */ + private function get_user(): void { + $this->user = get_user_by('id', $this->user_id); + + if (!$this->user) { + throw new \Exception("User with id {$this->user_id} not found"); + } + } + + /** + * Check if a Hellotext profile already exists. + * + * @return bool + */ + private function verify_if_profile_exists(): bool { + $hellotext_profile_id = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); + + return false != $hellotext_profile_id && '' != $hellotext_profile_id; + } + + /** + * Determine if the session value has changed. + * + * @return bool + */ + private function session_changed(): bool { + return get_user_meta($this->user_id, Constants::META_SESSION, true) != $this->session; + } + + /** + * Create a Hellotext profile or reuse existing one. + * + * @return void + */ + public function create_hellotext_profile(): void { + $profile = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); + + if ($profile) { + if (isset($this->user)) { + update_user_meta($this->user->ID, Constants::META_PROFILE_ID, $profile); + } + + $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", [ 'session' => $this->session, 'profile' => $profile, - )); + ]); - return; - } + return; + } - $phone = get_user_meta($this->user_id, 'billing_phone', true); + $phone = get_user_meta($this->user_id, 'billing_phone', true); - $response = $this->client::post(Constants::API_ENDPOINT_PROFILES, array_filter(array( - 'session' => $this->session, - 'reference' => isset($this->user) ? $this->user->ID : null, - 'first_name' => $this->user->nickname ?? $this->billing['first_name'], - 'last_name' => $this->user->last_name ?? $this->billing['last_name'], - 'email' => $this->user->user_email ?? $this->billing['email'], - 'phone' => empty($phone) ? $this->billing['phone'] : $phone, - 'lists' => array('WooCommerce'), - ))); + $response = $this->client::post(Constants::API_ENDPOINT_PROFILES, array_filter([ + 'session' => $this->session, + 'reference' => isset($this->user) ? $this->user->ID : null, + 'first_name' => $this->user->nickname ?? $this->billing['first_name'], + 'last_name' => $this->user->last_name ?? $this->billing['last_name'], + 'email' => $this->user->user_email ?? $this->billing['email'], + 'phone' => empty($phone) ? $this->billing['phone'] : $phone, + 'lists' => ['WooCommerce'], + ])); - add_user_meta( $this->user_id ?? $this->session, Constants::META_PROFILE_ID, $response['body']['id'], true ); + add_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, $response['body']['id'], true); - $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", array( + $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", [ 'session' => $this->session, 'profile' => $response['body']['id'], - )); - } - - /** - * Attach profile ID to current session. - * - * @return void - */ - private function attach_profile_to_session (): void { - $profile_id = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); - - $response = $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", array( - 'session' => $this->session, - 'profile' => $profile_id, - )); - } + ]); + } + + /** + * Attach profile ID to current session. + * + * @return void + */ + private function attach_profile_to_session(): void { + $profile_id = get_user_meta($this->user_id ?? $this->session, Constants::META_PROFILE_ID, true); + + $response = $this->client::patch(Constants::API_ENDPOINT_SESSIONS . "/{$this->session}", [ + 'session' => $this->session, + 'profile' => $profile_id, + ]); + } } /** @@ -194,21 +194,21 @@ private function attach_profile_to_session (): void { * @return void */ add_action('hellotext_create_profile', function (mixed $payload = null): void { - if (is_array($payload)) { - ( new CreateProfile(null, $payload) )->process(); - } + if (is_array($payload)) { + (new CreateProfile(null, $payload))->process(); + } - if (!is_user_logged_in() && !isset($user_id)) { - return; - } + if (!is_user_logged_in() && !isset($user_id)) { + return; + } - $user = ( null != $user_id ) - ? get_user_by('id', $user_id) - : wp_get_current_user(); + $user = (null != $user_id) + ? get_user_by('id', $user_id) + : wp_get_current_user(); - if (!$user) { - return; - } + if (!$user) { + return; + } - ( new CreateProfile($user->ID) )->process(); + (new CreateProfile($user->ID))->process(); }, 10, 1); diff --git a/src/Services/Session.php b/src/Services/Session.php index cc65967..da85a0d 100644 --- a/src/Services/Session.php +++ b/src/Services/Session.php @@ -12,46 +12,45 @@ * @package Hellotext\Services */ class Session { - - /** - * Encrypt a session identifier. - * - * @param string|null $session Session identifier. - * @return string - */ - public static function encrypt (?string $session = null): string { - $key = get_option(Constants::OPTION_BUSINESS_ID); - - // Use empty string as fallback if key is not set - if (!$key) { - $key = ''; - } - - // Generate an initialization vector (IV) - $iv_length = openssl_cipher_iv_length(Constants::ENCRYPTION_METHOD); - $iv = openssl_random_pseudo_bytes($iv_length); - - $encrypted = openssl_encrypt($session ?? '', Constants::ENCRYPTION_METHOD, $key, 0, $iv); - - return base64_encode($encrypted . '::' . $iv); - } - - /** - * Decrypt an encrypted session identifier. - * - * @param string|null $encrypted_data Encrypted session data. - * @return string|false - */ - public static function decrypt (?string $encrypted_data = null): string|false { - $key = get_option(Constants::OPTION_BUSINESS_ID); - - // Use empty string as fallback if key is not set - if (!$key) { - $key = ''; - } - - $parts = explode('::', base64_decode($encrypted_data ?? '')); - - return openssl_decrypt($parts[0], Constants::ENCRYPTION_METHOD, $key, 0, $parts[1]); - } + /** + * Encrypt a session identifier. + * + * @param string|null $session Session identifier. + * @return string + */ + public static function encrypt(?string $session = null): string { + $key = get_option(Constants::OPTION_BUSINESS_ID); + + // Use empty string as fallback if key is not set + if (!$key) { + $key = ''; + } + + // Generate an initialization vector (IV) + $iv_length = openssl_cipher_iv_length(Constants::ENCRYPTION_METHOD); + $iv = openssl_random_pseudo_bytes($iv_length); + + $encrypted = openssl_encrypt($session ?? '', Constants::ENCRYPTION_METHOD, $key, 0, $iv); + + return base64_encode($encrypted . '::' . $iv); + } + + /** + * Decrypt an encrypted session identifier. + * + * @param string|null $encrypted_data Encrypted session data. + * @return string|false + */ + public static function decrypt(?string $encrypted_data = null): string|false { + $key = get_option(Constants::OPTION_BUSINESS_ID); + + // Use empty string as fallback if key is not set + if (!$key) { + $key = ''; + } + + $parts = explode('::', base64_decode($encrypted_data ?? '')); + + return openssl_decrypt($parts[0], Constants::ENCRYPTION_METHOD, $key, 0, $parts[1]); + } } From 390d13e1a443e47907e00f77c7f22cec9ea20cc7 Mon Sep 17 00:00:00 2001 From: rockwellll Date: Sun, 18 Jan 2026 13:35:14 +0300 Subject: [PATCH 10/13] adjust styling settings --- .php-cs-fixer.cache | 2 +- .php-cs-fixer.php | 57 +++++++++++++++++++++++++++++++++++++++------ src/Api/Client.php | 22 ++++++++--------- src/Api/Event.php | 12 +++++----- 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index 77d085e..32b6755 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -1 +1 @@ -{"php":"8.4.8","version":"3.92.5:v3.92.5#260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"classes_opening_brace":"same_line","functions_opening_brace":"same_line"},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"trailing_comma_in_multiline":true,"phpdoc_scalar":true,"phpdoc_single_line_var_spacing":true,"phpdoc_var_without_name":true,"single_quote":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"src\/Misc\/Settings.php":"5e71bd024792e74350c4f1fabcab8065","src\/Misc\/Scripts.php":"e6cad7cd6433d891904071e795d7b0b5","src\/Constants.php":"d7e09100a92074fca817f5e422cf49fb","src\/Adapters\/RefundAdapter.php":"b28b6632847c1a62156d56b54f9f7c21","src\/Adapters\/ProductAdapter.php":"cc0aca0113a5011e83c87ffee4e76b2f","src\/Adapters\/PriceAdapter.php":"f745ecde0eb052560f2db758e1c3122e","src\/Adapters\/OrderAdapter.php":"3ca1bd395d356e3b75e0ceea176cb524","src\/Api\/Event.php":"fc61dc264456555edafb8ec9f1ff4d74","src\/Api\/Webchat.php":"ef03b3ca436f233ea7ffd4e1bff0f576","src\/Api\/Client.php":"ca7ed30fe809babb8378f6986fdd9d20","src\/Events\/CartUpdates.php":"72e4d163177b381fabc2d683fd104542","src\/Events\/OrderPlaced.php":"1b39dc7198082d8189ce509041f18757","src\/Events\/OrderStatus.php":"6d120b5401afb4f186e9fac1da425bf5","src\/Events\/RefundReceived.php":"a84421a12bf84435de14aeae1442a4a7","src\/Events\/AppInstalled.php":"058e8a7e9aa362b58aa6322f562c30fe","src\/Events\/UserRegistered.php":"f228476df530692be96fb1bb89fd9a79","src\/Events\/CustomOptionsUpdated.php":"cdd61280578691aa394a98d5ad953779","src\/Events\/ProductViewed.php":"8b467111ef300987a48a227a3f8b2a81","src\/Events\/CouponRedeemed.php":"63a56657cfe882ab05a943ae455fe74d","src\/Events\/AppRemoved.php":"993fd99a136255b9261dc8c6460f175f","src\/Services\/Session.php":"5f112c07423d60c7d5f6ece58fbceed0","src\/Services\/CreateProfile.php":"2c3cc60eb9f6e4681e6b931ebdff5a40"}} \ No newline at end of file +{"php":"8.4.8","version":"3.92.5:v3.92.5#260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"classes_opening_brace":"same_line","functions_opening_brace":"same_line"},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["extra","throw","use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":{"space_before":"none"},"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"trailing_comma_in_multiline":true,"single_quote":true,"no_unused_imports":true,"concat_space":{"spacing":"one"},"method_chaining_indentation":true,"no_spaces_around_offset":true,"no_whitespace_before_comma_in_array":true,"whitespace_after_comma_in_array":true,"phpdoc_scalar":true,"phpdoc_single_line_var_spacing":true,"phpdoc_var_without_name":true,"phpdoc_align":{"align":"left"},"standardize_not_equals":true,"no_unneeded_control_parentheses":true,"no_unneeded_braces":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"src\/Misc\/Settings.php":"5e71bd024792e74350c4f1fabcab8065","src\/Misc\/Scripts.php":"e6cad7cd6433d891904071e795d7b0b5","src\/Constants.php":"d7e09100a92074fca817f5e422cf49fb","src\/Adapters\/RefundAdapter.php":"b28b6632847c1a62156d56b54f9f7c21","src\/Adapters\/ProductAdapter.php":"cc0aca0113a5011e83c87ffee4e76b2f","src\/Adapters\/PriceAdapter.php":"f745ecde0eb052560f2db758e1c3122e","src\/Adapters\/OrderAdapter.php":"3ca1bd395d356e3b75e0ceea176cb524","src\/Api\/Event.php":"d3b4144a54d899c86095fb0d9e33e7b6","src\/Api\/Webchat.php":"ef03b3ca436f233ea7ffd4e1bff0f576","src\/Api\/Client.php":"b7c6f6762ab21fcd3fd3639420901b37","src\/Events\/CartUpdates.php":"72e4d163177b381fabc2d683fd104542","src\/Events\/OrderPlaced.php":"1b39dc7198082d8189ce509041f18757","src\/Events\/OrderStatus.php":"6d120b5401afb4f186e9fac1da425bf5","src\/Events\/RefundReceived.php":"a84421a12bf84435de14aeae1442a4a7","src\/Events\/AppInstalled.php":"058e8a7e9aa362b58aa6322f562c30fe","src\/Events\/UserRegistered.php":"f228476df530692be96fb1bb89fd9a79","src\/Events\/CustomOptionsUpdated.php":"cdd61280578691aa394a98d5ad953779","src\/Events\/ProductViewed.php":"8b467111ef300987a48a227a3f8b2a81","src\/Events\/CouponRedeemed.php":"63a56657cfe882ab05a943ae455fe74d","src\/Events\/AppRemoved.php":"993fd99a136255b9261dc8c6460f175f","src\/Services\/Session.php":"5f112c07423d60c7d5f6ece58fbceed0","src\/Services\/CreateProfile.php":"2c3cc60eb9f6e4681e6b931ebdff5a40"}} \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 639bae5..ee7ebd6 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -7,19 +7,62 @@ return (new PhpCsFixer\Config()) ->setRules([ '@PSR12' => true, + + // Array/Hash syntax (like Ruby's modern syntax) 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => ['sort_algorithm' => 'alpha'], - 'no_unused_imports' => true, - 'not_operator_with_successor_space' => false, 'trailing_comma_in_multiline' => true, - 'phpdoc_scalar' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_var_without_name' => true, + + // String literals (single quotes like Ruby) 'single_quote' => true, - 'no_extra_blank_lines' => true, + + // Imports/Use statements (alphabetically ordered) + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + + // Spacing rules (match Ruby spacing preferences) + 'binary_operator_spaces' => ['default' => 'single_space'], + 'concat_space' => ['spacing' => 'one'], + 'method_chaining_indentation' => true, + 'no_spaces_around_offset' => true, + 'no_whitespace_before_comma_in_array' => true, + 'whitespace_after_comma_in_array' => true, + + // Empty lines and whitespace + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'extra', + 'throw', + 'use', + ], + ], + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'single_blank_line_at_eof' => true, + + // Braces (your preference) 'braces_position' => [ 'classes_opening_brace' => 'same_line', 'functions_opening_brace' => 'same_line', ], + + // Method/Function definitions + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + ], + 'return_type_declaration' => ['space_before' => 'none'], + + // PHPDoc + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'phpdoc_align' => ['align' => 'left'], + + // Operators + 'not_operator_with_successor_space' => false, + 'standardize_not_equals' => true, + + // Control structures + 'no_unneeded_control_parentheses' => true, + 'no_unneeded_braces' => true, ]) ->setFinder($finder); diff --git a/src/Api/Client.php b/src/Api/Client.php index 8fb235a..e0db10b 100644 --- a/src/Api/Client.php +++ b/src/Api/Client.php @@ -48,10 +48,10 @@ public static function request(string $method = 'GET', string $path = '/', array $access_token = get_option(Constants::OPTION_ACCESS_TOKEN); $args = [ - 'method' => strtoupper($method), + 'method' => strtoupper($method), 'timeout' => 15, 'headers' => [ - 'Content-Type' => 'application/json', + 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $access_token, ], 'sslverify' => true, @@ -78,12 +78,12 @@ public static function request(string $method = 'GET', string $path = '/', array return [ 'request' => [ 'method' => $method, - 'path' => $request_url, - 'data' => $data, + 'path' => $request_url, + 'data' => $data, ], - 'status' => 0, - 'body' => null, - 'error' => $response->get_error_message(), + 'status' => 0, + 'body' => null, + 'error' => $response->get_error_message(), ]; } @@ -103,12 +103,12 @@ public static function request(string $method = 'GET', string $path = '/', array return [ 'request' => [ 'method' => $method, - 'path' => $request_url, - 'data' => $data, + 'path' => $request_url, + 'data' => $data, ], 'status' => $status_code, - 'body' => !empty($body_raw) ? json_decode($body_raw, true) : null, - 'error' => null, + 'body' => !empty($body_raw) ? json_decode($body_raw, true) : null, + 'error' => null, ]; } diff --git a/src/Api/Event.php b/src/Api/Event.php index ef80232..1e33b3a 100644 --- a/src/Api/Event.php +++ b/src/Api/Event.php @@ -46,7 +46,7 @@ public function __construct(?string $session = null) { public function track(string $action, array $payload): void { $body = array_merge( [ - 'action' => $action, + 'action' => $action, 'session' => $this->session ?? $this->browser_session(), ], $payload @@ -55,13 +55,13 @@ public function track(string $action, array $payload): void { $response = wp_remote_post( $this->get_api_url(), [ - 'timeout' => 10, - 'blocking' => false, - 'headers' => [ - 'Content-Type' => 'application/json', + 'timeout' => 10, + 'blocking' => false, + 'headers' => [ + 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->hellotext_business_id, ], - 'body' => json_encode($body), + 'body' => json_encode($body), 'sslverify' => true, ] ); From 686da3b3a9beea0f1947588048f35d0ac5d254e3 Mon Sep 17 00:00:00 2001 From: rockwellll Date: Sun, 18 Jan 2026 13:36:52 +0300 Subject: [PATCH 11/13] bump actions/checkout --- .github/workflows/php.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 05be9f2..2c44796 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -19,7 +19,7 @@ jobs: php: ["8.2", "8.3", "8.4", "8.5"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 From c5d5d8e8a97c18eebb0e78c249059cf4f6052dbc Mon Sep 17 00:00:00 2001 From: rockwellll Date: Sun, 18 Jan 2026 13:38:10 +0300 Subject: [PATCH 12/13] add dependantbot setup --- .github/dependabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c8a4517 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "ci" + include: "scope" + open-pull-requests-limit: 5 + + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + include: "scope" + open-pull-requests-limit: 5 From 2a0fe7c87b97e9145820830a9481010ff0b13a1b Mon Sep 17 00:00:00 2001 From: rockwellll Date: Sun, 18 Jan 2026 16:24:50 +0300 Subject: [PATCH 13/13] new workflow for tagging --- .distignore | 26 ++++++++++++++++++++++++++ .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++ composer.json | 3 ++- 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 .distignore create mode 100644 .github/workflows/release.yml diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..aca5ad9 --- /dev/null +++ b/.distignore @@ -0,0 +1,26 @@ +# Version control +.git +.github +.gitignore +.distignore + +# Development +tests +phpunit.xml +composer.json +composer.lock +DEVELOPMENT.md +API.md + +# Build tools +node_modules +.env +.env.* + +# IDE +.vscode +.idea +.DS_Store + +# Logs +*.log diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1e99454 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + + - name: Install production dependencies + run: composer install --no-dev --optimize-autoloader + + - name: Create release zip + run: | + mkdir -p release + rsync -av --exclude-from=.distignore . release/hellotext-wordpress/ + cd release + zip -r hellotext-wordpress.zip hellotext-wordpress + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: release/hellotext-wordpress.zip + generate_release_notes: true diff --git a/composer.json b/composer.json index ad264fd..e1d05ad 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "scripts": { "test": "vendor/bin/pest", "format": "vendor/bin/php-cs-fixer fix", - "format:check": "vendor/bin/php-cs-fixer fix --dry-run --diff" + "format:check": "vendor/bin/php-cs-fixer fix --dry-run --diff", + "build": "composer install --no-dev --optimize-autoloader" } }