Plugin Directory

Changeset 3645504


Ignore:
Timestamp:
08/13/2026 01:45:23 PM (2 months ago)
Author:
xplodman
Message:

Release 1.0.1: WordPress 7.0 compatibility, HPOS support, and Asana sync fixes

Location:
shopflow-for-asana
Files:
17 added
8 edited

Legend:

Unmodified
Added
Removed
  • shopflow-for-asana/trunk/assets/js/xsffa-admin.js

    r3253553 r3645504  
    11jQuery(document).ready(function ($) {
     2    // Refresh the Project/Assignee dropdowns when the Workspace changes,
     3    // since their available options depend on which workspace is selected.
     4    $('#xsffa-field-workspace_id').on('change', function () {
     5        const workspaceId = $(this).val();
     6        const $project = $('#xsffa-field-project_id');
     7        const $assignee = $('#xsffa-field-assignee_id');
     8
     9        if (!workspaceId || (!$project.length && !$assignee.length)) {
     10            return;
     11        }
     12
     13        $project.prop('disabled', true);
     14        $assignee.prop('disabled', true);
     15
     16        $.ajax({
     17            url: xsffa_ajax.ajax_url,
     18            type: 'POST',
     19            data: {
     20                action: 'xsffa_get_workspace_dependents',
     21                workspace_id: workspaceId,
     22                _ajax_nonce: xsffa_ajax.nonce
     23            },
     24            success: function (response) {
     25                if (response.success) {
     26                    populateSelect($project, response.data.projects);
     27                    populateSelect($assignee, response.data.assignees);
     28                } else {
     29                    alert('Error: ' + response.data.message);
     30                }
     31            },
     32            error: function () {
     33                alert('Failed to load projects/assignees for this workspace.');
     34            },
     35            complete: function () {
     36                $project.prop('disabled', false);
     37                $assignee.prop('disabled', false);
     38            }
     39        });
     40    });
     41
     42    // Replaces a <select>'s options with the given { value: label } map,
     43    // using .text() (not string concatenation) so labels from the API
     44    // can't inject markup.
     45    function populateSelect($select, options) {
     46        if (!$select.length) {
     47            return;
     48        }
     49        $select.empty();
     50        $select.append($('<option>', { value: '', selected: true, disabled: true }).text('Select an option'));
     51        $.each(options, function (value, label) {
     52            $select.append($('<option>', { value: value }).text(label));
     53        });
     54    }
     55
    256    // Handle Sync button click for individual orders
    357    $(document).on('click', '.sync-order', function (e) {
  • shopflow-for-asana/trunk/includes/class-xsffa-asana-api.php

    r3253553 r3645504  
    2121         * @var string
    2222         */
    23         public const ASANA_ORDER_META_KEY = '_xclickpw_asana_task_id';
     23        public const ASANA_ORDER_META_KEY = '_xsffa_asana_task_id';
     24
     25        /**
     26         * Previous (incorrectly prefixed, copy-pasted from another plugin) meta
     27         * key. Kept read-only for backwards compatibility with orders synced
     28         * before this was fixed; never written to new orders.
     29         *
     30         * @var string
     31         */
     32        public const LEGACY_ASANA_ORDER_META_KEY = '_xclickpw_asana_task_id';
    2433
    2534        /**
    … …  
    5564         */
    5665        private string $asana_project_id;
     66
     67        /**
     68         * Asana workspace ID.
     69         *
     70         * @var string
     71         */
     72        private string $asana_workspace_id;
     73
     74        /**
     75         * Human-readable reason the most recent Asana API request failed, if any.
     76         *
     77         * @var string
     78         */
     79        private string $last_error = '';
     80
     81        /**
     82         * Gets the Asana task ID an order was synced with, checking the current
     83         * meta key first and falling back to the legacy key for orders synced
     84         * before it was renamed.
     85         *
     86         * @param WC_Order $order WooCommerce Order object.
     87         * @return string Asana task ID, or an empty string if never synced.
     88         */
     89        public static function get_synced_task_id( $order ): string {
     90                $task_id = $order->get_meta( self::ASANA_ORDER_META_KEY, true );
     91
     92                if ( ! $task_id ) {
     93                        $task_id = $order->get_meta( self::LEGACY_ASANA_ORDER_META_KEY, true );
     94                }
     95
     96                return $task_id;
     97        }
    5798
    5899        /**
    … …  
    68109         */
    69110        public function init_options(): void {
    70                 $this->asana_api_token  = xsffa_core()->settings->options['access_token'] ?? '';
    71                 $this->asana_project_id = xsffa_core()->settings->options['project_id'] ?? '';
     111                $this->asana_api_token    = xsffa_core()->settings->options['access_token'] ?? '';
     112                $this->asana_project_id   = xsffa_core()->settings->options['project_id'] ?? '';
     113                $this->asana_workspace_id = xsffa_core()->settings->options['workspace_id'] ?? '';
    72114        }
    73115
    … …  
    93135                                'status'     => self::ORDER_STATUSES,
    94136                                'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
     137                                        'relation' => 'AND',
    95138                                        array(
    96139                                                'key'     => self::ASANA_ORDER_META_KEY,
     140                                                'compare' => 'NOT EXISTS',
     141                                        ),
     142                                        array(
     143                                                'key'     => self::LEGACY_ASANA_ORDER_META_KEY,
    97144                                                'compare' => 'NOT EXISTS',
    98145                                        ),
    … …  
    135182                                'status'     => self::ORDER_STATUSES,
    136183                                'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
     184                                        'relation' => 'AND',
    137185                                        array(
    138186                                                'key'     => self::ASANA_ORDER_META_KEY,
     187                                                'compare' => 'NOT EXISTS',
     188                                        ),
     189                                        array(
     190                                                'key'     => self::LEGACY_ASANA_ORDER_META_KEY,
    139191                                                'compare' => 'NOT EXISTS',
    140192                                        ),
    … …  
    161213                }
    162214
    163                 // Get orders without Asana task ID meta.
     215                // Get orders with an Asana task ID meta (current or legacy key).
    164216                $orders = wc_get_orders(
    165217                        array(
    166                                 'limit'        => self::MAX_ORDERS_PER_REQUEST,
    167                                 'offset'       => $offset,
    168                                 'status'       => self::ORDER_STATUSES,
    169                                 'meta_key'     => self::ASANA_ORDER_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
    170                                 'meta_compare' => 'EXISTS',
     218                                'limit'      => self::MAX_ORDERS_PER_REQUEST,
     219                                'offset'     => $offset,
     220                                'status'     => self::ORDER_STATUSES,
     221                                'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
     222                                        'relation' => 'OR',
     223                                        array(
     224                                                'key'     => self::ASANA_ORDER_META_KEY,
     225                                                'compare' => 'EXISTS',
     226                                        ),
     227                                        array(
     228                                                'key'     => self::LEGACY_ASANA_ORDER_META_KEY,
     229                                                'compare' => 'EXISTS',
     230                                        ),
     231                                ),
    171232                        )
    172233                );
    … …  
    202263                $remaining_orders = wc_get_orders(
    203264                        array(
    204                                 'limit'        => -1,
    205                                 'offset'       => $offset,
    206                                 'status'       => self::ORDER_STATUSES,
    207                                 'meta_key'     => self::ASANA_ORDER_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
    208                                 'meta_compare' => 'EXISTS',
    209                                 'return'       => 'ids',
     265                                'limit'      => -1,
     266                                'offset'     => $offset,
     267                                'status'     => self::ORDER_STATUSES,
     268                                'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
     269                                        'relation' => 'OR',
     270                                        array(
     271                                                'key'     => self::ASANA_ORDER_META_KEY,
     272                                                'compare' => 'EXISTS',
     273                                        ),
     274                                        array(
     275                                                'key'     => self::LEGACY_ASANA_ORDER_META_KEY,
     276                                                'compare' => 'EXISTS',
     277                                        ),
     278                                ),
     279                                'return'     => 'ids',
    210280                        )
    211281                );
    … …  
    220290         */
    221291        public function sync_order( int $order_id ) {
     292                $this->last_error = '';
     293
    222294                if ( ! $order_id ) {
     295                        $this->last_error = 'Invalid order ID.';
    223296                        return null;
    224297                }
    225298
    226299                if ( ! function_exists( 'wc_get_order' ) ) {
     300                        $this->last_error = 'WooCommerce is not active.';
    227301                        return null;
    228302                }
    … …  
    230304                $order = wc_get_order( $order_id );
    231305                if ( ! $order ) {
     306                        $this->last_error = 'Order not found.';
    232307                        return null;
    233308                }
    234309
    235310                // Prevent duplicate sync (check if already synced).
    236                 if ( $order->get_meta( self::ASANA_ORDER_META_KEY ) ) {
     311                if ( self::get_synced_task_id( $order ) ) {
     312                        $this->last_error = 'Order is already synced to Asana.';
    237313                        return null;
    238314                }
    … …  
    248324                }
    249325
    250                 return $response['data']['gid'];
     326                return $response['data']['gid'] ?? null;
    251327        }
    252328
    … …  
    262338                // Store Asana task ID in order meta.
    263339                $order->update_meta_data( self::ASANA_ORDER_META_KEY, $asana_task_id );
     340                // Clean up the legacy key so this order is fully migrated.
     341                $order->delete_meta_data( self::LEGACY_ASANA_ORDER_META_KEY );
    264342
    265343                // Add order note with Asana task link.
    … …  
    345423         * Send request to Asana API.
    346424         *
    347          * @param string $endpoint API endpoint (e.g., 'tasks').
     425         * @param string $endpoint API endpoint (e.g., 'tasks'), or a full URL (e.g. a pagination `next_page.uri`).
    348426         * @param array  $data Data to send in the request.
    349427         * @param string $method HTTP method (GET, POST, PUT, etc.).
    … …  
    351429         */
    352430        private function send_asana_request( string $endpoint, array $data = array(), string $method = 'GET' ) {
    353                 $url = "{$this->asana_api_url}/{$endpoint}";
     431                $url = ( 0 === strpos( $endpoint, 'http' ) ) ? $endpoint : "{$this->asana_api_url}/{$endpoint}";
    354432
    355433                $args = array(
    … …  
    368446
    369447                if ( is_wp_error( $response ) ) {
     448                        $this->last_error = $response->get_error_message();
    370449                        return false;
    371450                }
    372451
    373                 return json_decode( wp_remote_retrieve_body( $response ), true );
     452                $status = wp_remote_retrieve_response_code( $response );
     453                $body   = json_decode( wp_remote_retrieve_body( $response ), true );
     454
     455                if ( $status < 200 || $status >= 300 ) {
     456                        $this->last_error = $body['errors'][0]['message'] ?? "Asana API returned HTTP {$status}.";
     457                        return false;
     458                }
     459
     460                $this->last_error = '';
     461
     462                return $body;
     463        }
     464
     465        /**
     466         * Gets the reason the most recent Asana API request failed.
     467         *
     468         * @return string Human-readable error message, or an empty string if the last request succeeded.
     469         */
     470        public function get_last_error(): string {
     471                return $this->last_error;
     472        }
     473
     474        /**
     475         * Maximum number of pages to follow for a single paginated list request.
     476         * At 100 items per page, this covers up to 5,000 results — a safety
     477         * cap so a runaway `next_page` chain can't loop indefinitely.
     478         */
     479        private const MAX_PAGES = 50;
     480
     481        /**
     482         * Fetches every page of a paginated Asana "list" endpoint (workspaces,
     483         * projects, users), following `next_page.uri` until exhausted.
     484         *
     485         * @param string $endpoint API endpoint, e.g. 'projects?workspace=123'.
     486         * @return array Combined `data` items from every page, or an empty array on failure.
     487         */
     488        private function fetch_all_pages( string $endpoint ): array {
     489                $separator = ( false === strpos( $endpoint, '?' ) ) ? '?' : '&';
     490                $url       = $endpoint . $separator . 'limit=100';
     491
     492                $items = array();
     493
     494                for ( $page = 0; $url && $page < self::MAX_PAGES; $page++ ) {
     495                        $response = $this->send_asana_request( $url );
     496
     497                        if ( ! $response ) {
     498                                break;
     499                        }
     500
     501                        $items = array_merge( $items, $response['data'] ?? array() );
     502                        $url   = $response['next_page']['uri'] ?? null;
     503                }
     504
     505                return $items;
     506        }
     507
     508        /**
     509         * Retrieves workspaces from Asana API.
     510         *
     511         * @return array List of workspaces or an empty array on failure.
     512         */
     513        public function retrieve_workspaces(): array {
     514                $data = $this->fetch_all_pages( 'workspaces' );
     515
     516                $result = array();
     517                foreach ( $data as $workspace ) {
     518                        $result[ $workspace['gid'] ] = $workspace['name'];
     519                }
     520
     521                return $result;
    374522        }
    375523
    … …  
    377525         * Retrieves projects from Asana API.
    378526         *
     527         * @param string|null $workspace_id Workspace to look up projects for. Defaults to the saved workspace setting.
    379528         * @return array List of projects or an empty array on failure.
    380529         */
    381         public function retrieve_projects(): array {
    382                 $response = $this->send_asana_request( 'projects' );
    383 
    384                 if ( ! $response ) {
     530        public function retrieve_projects( ?string $workspace_id = null ): array {
     531                $workspace_id = $workspace_id ?? $this->asana_workspace_id;
     532
     533                if ( empty( $workspace_id ) ) {
     534                        $this->last_error = 'Select a workspace first.';
    385535                        return array();
    386536                }
    387537
    388                 $data = $response['data'] ?? array();
     538                $data = $this->fetch_all_pages( 'projects?workspace=' . rawurlencode( $workspace_id ) );
    389539
    390540                $result = array();
    … …  
    402552         */
    403553        public function resync_order( int $order_id ) {
     554                $this->last_error = '';
     555
    404556                if ( ! function_exists( 'wc_get_order' ) ) {
     557                        $this->last_error = 'WooCommerce is not active.';
    405558                        return null;
    406559                }
    … …  
    408561                $order = wc_get_order( $order_id );
    409562                if ( ! $order ) {
     563                        $this->last_error = 'Order not found.';
    410564                        return null;
    411565                }
    412566
    413567                // Get current asana_task_id.
    414                 $asana_task_id = $order->get_meta( self::ASANA_ORDER_META_KEY, true );
     568                $asana_task_id = self::get_synced_task_id( $order );
    415569
    416570                // Update Asana task if it exists.
    … …  
    443597                        }
    444598
    445                         return $response['data']['gid'];
    446                 }
    447 
     599                        return $response['data']['gid'] ?? null;
     600                }
     601
     602                $this->last_error = 'Order has not been synced to Asana yet; nothing to resync.';
    448603                return null;
    449604        }
    … …  
    452607         * Retrieves users (assignees) from Asana API.
    453608         *
     609         * @param string|null $workspace_id Workspace to look up users for. Defaults to the saved workspace setting.
    454610         * @return array List of users or an empty array on failure.
    455611         */
    456         public function retrieve_assignees(): array {
    457                 $response = $this->send_asana_request( 'users' );
    458 
    459                 if ( ! $response ) {
     612        public function retrieve_assignees( ?string $workspace_id = null ): array {
     613                $workspace_id = $workspace_id ?? $this->asana_workspace_id;
     614
     615                if ( empty( $workspace_id ) ) {
     616                        $this->last_error = 'Select a workspace first.';
    460617                        return array();
    461618                }
    462619
    463                 $data = $response['data'] ?? array();
     620                $data = $this->fetch_all_pages( 'users?workspace=' . rawurlencode( $workspace_id ) );
    464621
    465622                $result = array();
  • shopflow-for-asana/trunk/includes/class-xsffa-core.php

    r3253553 r3645504  
    8282                $this->handler   = new XSFFA_Handler();
    8383                $this->asana_api = new XSFFA_Asana_Api();
    84 
    85                 add_action( 'init', array( $this, 'load_textdomain' ) );
    8684        }
    8785
    … …  
    10098
    10199        /**
    102          * Loads the plugin's text domain for translations.
    103          *
    104          * @return void
    105          */
    106         public function load_textdomain() {
    107                 load_plugin_textdomain( 'shopflow-for-asana', false, XSFFA_PLUGIN_REL_PATH . 'languages/' );
    108         }
    109 
    110         /**
    111100         * Is the current page is plugin page?
    112101         *
  • shopflow-for-asana/trunk/includes/class-xsffa-orders-table.php

    r3253553 r3645504  
    55 * @package shopflow-for-asana
    66 */
     7
     8if ( ! defined( 'ABSPATH' ) ) {
     9        exit;
     10}
    711
    812// WP_List_Table is not loaded automatically so we need to load it in our application.
  • shopflow-for-asana/trunk/includes/class-xsffa-settings.php

    r3253553 r3645504  
    3939                'auto_sync'             => '1',
    4040                'access_token'          => '',
     41                'workspace_id'          => '',
    4142                'project_id'            => '',
    4243                'assignee_id'           => '',
    … …  
    5253                'auto_sync'             => 'bool', // Auto sync.
    5354                'access_token'          => 'string', // API key.
     55                'workspace_id'          => 'select', // Workspace ID.
    5456                'project_id'            => 'select', // Project ID.
    5557                'assignee_id'           => 'select', // Assignee ID.
    … …  
    6567                'auto_sync'             => 'Enable this to automatically sync WooCommerce orders to Asana.',
    6668                'access_token'          => 'Enter your Asana Personal Access Token. Create a token from the <a href="https://app.asana.com/0/developer-console" target="_blank" rel="noopener noreferrer">Asana Developer Console</a>.',
     69                'workspace_id'          => 'Choose the Asana Workspace to sync orders into. Required if your Asana account belongs to more than one workspace.',
    6770                'project_id'            => 'Choose the Asana Project where orders should be synced.',
    6871                'assignee_id'           => 'Choose the Asana User who should be assigned to the task.',
    … …  
    99102                add_action( 'wp_ajax_xsffa_sync_single_order', array( $this, 'xsffa_sync_single_order' ) );
    100103                add_action( 'wp_ajax_xsffa_resync_single_order', array( $this, 'xsffa_resync_single_order' ) );
     104                add_action( 'wp_ajax_xsffa_get_workspace_dependents', array( $this, 'xsffa_get_workspace_dependents' ) );
     105        }
     106
     107        /**
     108         * Handles AJAX requests to refresh the Project/Assignee dropdowns when
     109         * the selected Workspace changes, before the settings form is saved.
     110         *
     111         * @return void
     112         */
     113        public function xsffa_get_workspace_dependents() {
     114                check_ajax_referer( 'xsffa_sync_orders_nonce' );
     115
     116                $workspace_id = isset( $_POST['workspace_id'] ) ? sanitize_text_field( wp_unslash( $_POST['workspace_id'] ) ) : '';
     117
     118                if ( ! $workspace_id ) {
     119                        wp_send_json_error( array( 'message' => 'Missing workspace ID.' ) );
     120                }
     121
     122                wp_send_json_success(
     123                        array(
     124                                'projects'  => xsffa_core()->asana_api->retrieve_projects( $workspace_id ),
     125                                'assignees' => xsffa_core()->asana_api->retrieve_assignees( $workspace_id ),
     126                        )
     127                );
    101128        }
    102129
    … …  
    145172                        'auto_sync'             => esc_html__( 'Auto Sync', 'shopflow-for-asana' ),
    146173                        'access_token'          => esc_html__( 'Personal Access Token', 'shopflow-for-asana' ),
     174                        'workspace_id'          => esc_html__( 'Workspace', 'shopflow-for-asana' ),
    147175                        'project_id'            => esc_html__( 'Project ID', 'shopflow-for-asana' ),
    148176                        'assignee_id'           => esc_html__( 'Assignee ID', 'shopflow-for-asana' ),
    … …  
    235263                                if ( isset( $this->select_options[ $key ] ) && is_array( $this->select_options[ $key ] ) ) {
    236264                                        printf(
    237                                                 '<select %s required name="xsffa_settings[%s]">',
     265                                                '<select id="xsffa-field-%s" %s required name="xsffa_settings[%s]">',
     266                                                esc_attr( $key ),
    238267                                                esc_attr( $disabled ),
    239268                                                esc_attr( $key )
    … …  
    319348         */
    320349        private function disable_check_project_id(): bool {
     350                return empty( $this->options['access_token'] ) || empty( $this->options['workspace_id'] );
     351        }
     352
     353        /**
     354         * Disables the "Workspace" setting if the Asana access token is not set.
     355         *
     356         * @return bool True if the setting should be disabled, false otherwise.
     357         */
     358        private function disable_check_workspace_id(): bool {
    321359                return empty( $this->options['access_token'] );
    322360        }
    … …  
    330368         */
    331369        private function disable_check_assignee_id(): bool {
    332                 return empty( $this->options['access_token'] );
     370                return empty( $this->options['access_token'] ) || empty( $this->options['workspace_id'] );
    333371        }
    334372
    … …  
    700738
    701739        /**
     740         * Retrieves workspaces from Asana API.
     741         *
     742         * @return array List of workspaces or an empty array on failure.
     743         */
     744        private function get_options_workspace_id(): array {
     745                return xsffa_core()->asana_api->retrieve_workspaces();
     746        }
     747
     748        /**
    702749         * Retrieves projects from Asana API.
    703750         *
    … …  
    746793
    747794                        // Check if the order is synced (has an Asana task ID).
    748                         if ( $order->get_meta( XSFFA_Asana_Api::ASANA_ORDER_META_KEY, true ) ) {
     795                        if ( XSFFA_Asana_Api::get_synced_task_id( $order ) ) {
    749796                                ++$synced_orders[ $date ];
    750797                        }
    … …  
    905952                        wp_send_json_success( array( 'task_id' => $task_id ) );
    906953                } else {
    907                         wp_send_json_error( array( 'message' => 'Failed to sync order.' ) );
     954                        $reason = xsffa_core()->asana_api->get_last_error();
     955                        wp_send_json_error( array( 'message' => $reason ? "Failed to sync order: {$reason}" : 'Failed to sync order.' ) );
    908956                }
    909957        }
    … …  
    928976                        wp_send_json_success( array( 'task_id' => $task_id ) );
    929977                } else {
    930                         wp_send_json_error( array( 'message' => 'Failed to resync order.' ) );
     978                        $reason = xsffa_core()->asana_api->get_last_error();
     979                        wp_send_json_error( array( 'message' => $reason ? "Failed to resync order: {$reason}" : 'Failed to resync order.' ) );
    931980                }
    932981        }
  • shopflow-for-asana/trunk/includes/helpers.php

    r3253553 r3645504  
    55 * @package  shopflow-for-asana
    66 */
     7
     8if ( ! defined( 'ABSPATH' ) ) {
     9        exit;
     10}
    711
    812if ( ! function_exists( 'xsffa_sanitize_settings' ) ) {
    … …  
    7781                $order_date    = $order->get_date_created()->date( 'Y-m-d H:i:s' );
    7882                $status        = $order->get_status();
    79                 $asana_task_id = $order->get_meta( XSFFA_Asana_Api::ASANA_ORDER_META_KEY, true );
     83                $asana_task_id = XSFFA_Asana_Api::get_synced_task_id( $order );
    8084
    8185                return array(
  • shopflow-for-asana/trunk/readme.txt

    r3253553 r3645504  
    33Tags: asana, woocommerce, order sync, task management, automation
    44Requires at least: 5.6
    5 Tested up to: 6.7
     5Tested up to: 7.0
    66Requires PHP: 7.4
    7 Stable tag: 1.0.0
     7Stable tag: 1.0.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    6363== Changelog ==
    6464
     65= 1.0.1 =
     66* Confirmed compatibility with WordPress 7.0.
     67* Declared compatibility with WooCommerce High-Performance Order Storage (HPOS).
     68* Added a Workspace setting, required for Asana accounts that belong to more than one workspace. Fixes the Project/Assignee dropdowns showing no options for such accounts, and fixes order sync failing silently in the same situation.
     69* The Project and Assignee dropdowns now refresh live via AJAX when the Workspace selection changes, instead of only updating after saving the page.
     70* Project, Assignee, and Workspace lists are now fetched across all pages from Asana, instead of only the first page.
     71* Order sync errors now show the actual reason from Asana (e.g. an invalid token) instead of a single generic "Failed to sync order" message for every failure.
     72* Fixed PHP warnings on order sync failure caused by an unguarded array access.
     73* Fixed the order-sync meta key using the wrong plugin's prefix (a copy-paste leftover); orders already synced under the old key are still recognized via a compatibility fallback, so nothing needs to be re-synced.
     74* Removed discouraged `load_plugin_textdomain()` call (translations are auto-loaded by WordPress.org since 4.6).
     75* Added missing direct-file-access guards in `includes/helpers.php` and `includes/class-xsffa-orders-table.php`.
     76
    6577= 1.0.0 =
    6678* Initial release.
    … …  
    7284== Upgrade Notice ==
    7385
     86= 1.0.1 =
     87Compatibility update for WordPress 7.0 and WooCommerce HPOS, plus more reliable order sync error handling. Safe, non-breaking update.
     88
    7489= 1.0.0 =
    7590Upgrade now to automate your WooCommerce order management with Asana. Turn orders into tasks effortlessly!
  • shopflow-for-asana/trunk/shopflow-for-asana.php

    r3253553 r3645504  
    44 * Requires Plugins: woocommerce
    55 * Description: Automatically sync WooCommerce orders to Asana tasks.
    6  * Version: 1.0.0
     6 * Version: 1.0.1
    77 * Author: Xplodman
    88 * Author URI: https://github.com/xplodman/
    … …  
    2222define( 'XSFFA_PLUGIN_REL_PATH', dirname( XSFFA_PLUGIN_BASEDIR ) . '/' );
    2323define( 'XSFFA_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
     24
     25// Declare compatibility with WooCommerce High-Performance Order Storage (HPOS).
     26// This plugin only ever accesses order data through wc_get_order()/wc_get_orders(),
     27// so it doesn't touch the legacy post tables directly and is HPOS-safe.
     28add_action(
     29        'before_woocommerce_init',
     30        function () {
     31                if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
     32                        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
     33                }
     34        }
     35);
    2436
    2537// Autoload classes.
Note: See TracChangeset for help on using the changeset viewer.