Plugin Directory

Changeset 3435486


Ignore:
Timestamp:
01/08/2026 09:20:09 PM (6 weeks ago)
Author:
hosseinkarami
Message:

Update to version 1.1.0 - Add Shopify OAuth support and improvements

Location:
products-showcase/trunk
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • products-showcase/trunk/README.md

    r3408553 r3435486  
    33A powerful WordPress plugin that displays Shopify products and collections in beautiful, responsive carousels using **native Gutenberg blocks**.
    44
    5 ![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)
     5![Version](https://img.shields.io/badge/version-1.1.0-blue.svg)
    66![WordPress](https://img.shields.io/badge/wordpress-6.0%2B-blue.svg)
    77![PHP](https://img.shields.io/badge/php-8.1%2B-purple.svg)
     
    4949**Data Transmitted to Shopify**:
    50501. **Shopify Store URL** - Your store's domain (e.g., `your-store.myshopify.com`) configured in plugin settings
    51 2. **Admin API Access Token** - Your Shopify Admin API authentication token (configured in plugin settings)
    52 3. **GraphQL Queries** - Specific queries requesting product and collection data
     512. **OAuth Credentials** - Client ID and Client Secret are used during the one-time OAuth authorization flow
     523. **Admin API Access Token** - Obtained automatically via OAuth and used for all subsequent API requests
     534. **GraphQL Queries** - Specific queries requesting product and collection data
    5354
    5455**When Data is Transmitted**:
     
    135136## ⚙️ Configuration
    136137
    137 ### 1. Get Shopify API Credentials
     138### 1. Create a Shopify Custom App
    138139
    1391401. Log in to your **Shopify Admin**
    1401412. Navigate to **Settings → Apps and sales channels**
    1411423. Click **"Develop apps"**
    142 4. **Create a new app** or select existing
    143 5. Configure **Admin API scopes**:
     1434. Click **"Create an app"** and give it a name (e.g., "WordPress Integration")
     144
     145### 2. Configure API Access
     146
     1471. In your app, go to the **"Configuration"** tab
     1482. Under **"Admin API integration"**, click **"Configure"**
     1493. Enable the following scope:
    144150   - ✅ `read_products` (required)
    145 6. **Install the app** to your store
    146 7. Copy the **Admin API access token**
    147 
    148 ### 2. Configure Plugin Settings
    149 
    150 1. Go to **Settings → Shopify Products** in WordPress admin
     1514. Click **"Save"**
     1525. **Important**: Under **"Allowed redirection URL(s)"**, add the Redirect URL shown in your WordPress plugin settings
     153
     154### 3. Get Your Credentials
     155
     1561. Go to the **"API credentials"** tab in your Shopify app
     1572. Copy the **Client ID**
     1583. Copy the **Client secret**
     159
     160### 4. Connect via OAuth (Easy Setup!)
     161
     1621. Go to **Shopify Products** in WordPress admin
    1511632. Enter your **Shopify Store URL** (e.g., `your-store.myshopify.com`)
    152 3. Paste your **Admin API Access Token**
    153 4. Set **Cache Duration** (default: 1 hour)
    154 5. Click **Save Settings**
    155 
    156 The plugin will test the connection and show you a success message if configured correctly.
     1643. Paste your **Client ID**
     1654. Paste your **Client Secret**
     1665. Click **"Connect to Shopify"**
     1676. You'll be redirected to Shopify to authorize the connection
     1687. After authorizing, you'll be redirected back to WordPress - done! ✅
     169
     170The plugin automatically:
     171- Obtains the access token via secure OAuth
     172- Detects the latest supported Shopify API version
     173- Tests the connection and displays your store name
    157174
    158175## 📖 Usage
  • products-showcase/trunk/assets/admin/admin.css

    r3408553 r3435486  
    344344.prodshow-connection-status-card.error .prodshow-status-value {
    345345    color: #dc3545;
     346}
     347
     348.prodshow-auto-detected {
     349    font-size: 0.85em;
     350    color: #28a745;
     351    font-weight: normal;
     352}
     353
     354.prodshow-fallback-version {
     355    font-size: 0.85em;
     356    color: #dc3545;
     357    font-weight: normal;
     358}
     359
     360.prodshow-api-version-display {
     361    display: flex;
     362    align-items: center;
     363    gap: 8px;
     364    flex-wrap: wrap;
     365}
     366
     367#prodshow-refresh-api-version {
     368    padding: 2px 6px;
     369    min-height: auto;
     370    line-height: 1;
     371}
     372
     373#prodshow-refresh-api-version .dashicons {
     374    font-size: 14px;
     375    width: 14px;
     376    height: 14px;
     377}
     378
     379.prodshow-spin {
     380    animation: prodshow-spin 1s linear infinite;
     381}
     382
     383@keyframes prodshow-spin {
     384    from { transform: rotate(0deg); }
     385    to { transform: rotate(360deg); }
    346386}
    347387
     
    937977    display: none;
    938978}
     979
     980/* OAuth Section Styles */
     981#prodshow-oauth-section .required {
     982    color: #dc3545;
     983    font-weight: bold;
     984}
     985
     986.prodshow-oauth-connected-info {
     987    display: flex;
     988    align-items: center;
     989    justify-content: space-between;
     990    padding: 20px;
     991    background: #f0f9f0;
     992    border-top: 1px solid #e5e5e5;
     993}
     994
     995.prodshow-oauth-status {
     996    display: flex;
     997    align-items: center;
     998    gap: 8px;
     999    margin: 0;
     1000    font-size: 1rem;
     1001    font-weight: 600;
     1002    color: #28a745;
     1003}
     1004
     1005.prodshow-oauth-status .dashicons {
     1006    font-size: 24px;
     1007    width: 24px;
     1008    height: 24px;
     1009}
     1010
     1011.prodshow-redirect-url-box {
     1012    display: flex;
     1013    align-items: center;
     1014    gap: 8px;
     1015    background: #f9f9f9;
     1016    padding: 8px 12px;
     1017    border: 1px solid #ddd;
     1018    border-radius: 4px;
     1019    max-width: 100%;
     1020    overflow-x: auto;
     1021}
     1022
     1023.prodshow-redirect-url-box code {
     1024    background: transparent;
     1025    padding: 0;
     1026    font-size: 13px;
     1027    word-break: break-all;
     1028    flex: 1;
     1029}
     1030
     1031#prodshow-copy-redirect-url {
     1032    flex-shrink: 0;
     1033    padding: 4px 8px;
     1034    min-height: auto;
     1035    line-height: 1;
     1036}
     1037
     1038#prodshow-copy-redirect-url .dashicons {
     1039    font-size: 16px;
     1040    width: 16px;
     1041    height: 16px;
     1042}
     1043
     1044#prodshow-copy-redirect-url.copied {
     1045    background: #28a745;
     1046    border-color: #28a745;
     1047    color: #fff;
     1048}
     1049
     1050.prodshow-oauth-actions {
     1051    display: flex;
     1052    align-items: center;
     1053    gap: 12px;
     1054    padding: 24px;
     1055    background: #f9f9f9;
     1056    border-top: 1px solid #e5e5e5;
     1057}
     1058
     1059.prodshow-connect-button {
     1060    display: inline-flex !important;
     1061    align-items: center;
     1062    gap: 10px;
     1063    padding: 14px 28px !important;
     1064    font-size: 15px !important;
     1065    background: #95BF46 !important;
     1066    border-color: #95BF46 !important;
     1067}
     1068
     1069.prodshow-connect-button::before {
     1070    content: none !important;
     1071}
     1072
     1073.prodshow-connect-button:hover,
     1074.prodshow-connect-button:focus {
     1075    background: #7aa93a !important;
     1076    border-color: #7aa93a !important;
     1077}
     1078
     1079.prodshow-connect-button:disabled {
     1080    opacity: 0.7;
     1081    cursor: not-allowed;
     1082}
     1083
     1084.prodshow-connect-button svg {
     1085    flex-shrink: 0;
     1086}
     1087
     1088.prodshow-oauth-spinner {
     1089    float: none !important;
     1090    margin: 0 !important;
     1091}
     1092
     1093/* Responsive adjustments for OAuth */
     1094@media screen and (max-width: 782px) {
     1095    .prodshow-oauth-connected-info {
     1096        flex-direction: column;
     1097        gap: 16px;
     1098        text-align: center;
     1099    }
     1100
     1101    .prodshow-redirect-url-box {
     1102        flex-direction: column;
     1103        align-items: stretch;
     1104    }
     1105
     1106    .prodshow-redirect-url-box code {
     1107        text-align: center;
     1108    }
     1109
     1110    #prodshow-copy-redirect-url {
     1111        width: 100%;
     1112    }
     1113
     1114    .prodshow-oauth-actions {
     1115        flex-direction: column;
     1116    }
     1117
     1118    .prodshow-connect-button {
     1119        width: 100%;
     1120        justify-content: center;
     1121    }
     1122}
  • products-showcase/trunk/assets/admin/admin.js

    r3408553 r3435486  
    285285        });
    286286
     287        /**
     288         * OAuth Connect to Shopify
     289         */
     290        $('#prodshow-connect-btn').on('click', function(e) {
     291            e.preventDefault();
     292           
     293            var $btn = $(this);
     294            var $spinner = $('.prodshow-oauth-spinner');
     295            var nonce = $('#prodshow-oauth-nonce').val();
     296           
     297            // Get form values
     298            var shopUrl = $('#prodshow_shopify_url').val().trim();
     299            var clientId = $('#prodshow_shopify_client_id').val().trim();
     300            var clientSecret = $('#prodshow_shopify_client_secret').val().trim();
     301           
     302            // Validate fields
     303            if (!shopUrl) {
     304                alert('Please enter your Shopify store URL.');
     305                $('#prodshow_shopify_url').focus();
     306                return;
     307            }
     308           
     309            if (!clientId) {
     310                alert('Please enter your Client ID.');
     311                $('#prodshow_shopify_client_id').focus();
     312                return;
     313            }
     314           
     315            if (!clientSecret) {
     316                alert('Please enter your Client Secret.');
     317                $('#prodshow_shopify_client_secret').focus();
     318                return;
     319            }
     320           
     321            // Validate shop URL format
     322            if (!shopUrl.match(/^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com$/)) {
     323                alert('Please enter a valid Shopify store URL (e.g., your-store.myshopify.com)');
     324                $('#prodshow_shopify_url').focus();
     325                return;
     326            }
     327           
     328            // Show loading state
     329            $btn.prop('disabled', true);
     330            $spinner.addClass('is-active');
     331           
     332            // Make AJAX request to initiate OAuth
     333            $.ajax({
     334                url: ajaxurl,
     335                type: 'POST',
     336                data: {
     337                    action: 'prodshow_initiate_oauth',
     338                    nonce: nonce,
     339                    shop_url: shopUrl,
     340                    client_id: clientId,
     341                    client_secret: clientSecret
     342                },
     343                success: function(response) {
     344                    if (response.success && response.data.redirect_url) {
     345                        // Redirect to Shopify for authorization
     346                        window.location.href = response.data.redirect_url;
     347                    } else {
     348                        alert(response.data.message || 'Failed to initiate connection. Please try again.');
     349                        $btn.prop('disabled', false);
     350                        $spinner.removeClass('is-active');
     351                    }
     352                },
     353                error: function(xhr, status, error) {
     354                    console.error('OAuth Error:', error);
     355                    alert('Connection error. Please try again.');
     356                    $btn.prop('disabled', false);
     357                    $spinner.removeClass('is-active');
     358                }
     359            });
     360        });
     361
     362        /**
     363         * Disconnect from Shopify
     364         */
     365        $('#prodshow-disconnect-btn').on('click', function(e) {
     366            e.preventDefault();
     367           
     368            if (!confirm('Are you sure you want to disconnect from Shopify? You will need to reconnect to continue using the plugin.')) {
     369                return;
     370            }
     371           
     372            var $btn = $(this);
     373            var nonce = $('#prodshow-oauth-nonce').val() || $('input[name="_wpnonce"]').val();
     374           
     375            // If no nonce found, create one from the hidden field
     376            if (!nonce) {
     377                // Try to get it from the page
     378                nonce = $('#prodshow-clear-cache-nonce').val();
     379            }
     380           
     381            $btn.prop('disabled', true).text('Disconnecting...');
     382           
     383            $.ajax({
     384                url: ajaxurl,
     385                type: 'POST',
     386                data: {
     387                    action: 'prodshow_disconnect_shopify',
     388                    nonce: nonce
     389                },
     390                success: function(response) {
     391                    if (response.success) {
     392                        // Reload the page to show disconnected state
     393                        window.location.reload();
     394                    } else {
     395                        alert(response.data.message || 'Failed to disconnect. Please try again.');
     396                        $btn.prop('disabled', false).text('Disconnect');
     397                    }
     398                },
     399                error: function() {
     400                    alert('Connection error. Please try again.');
     401                    $btn.prop('disabled', false).text('Disconnect');
     402                }
     403            });
     404        });
     405
     406        /**
     407         * Copy Redirect URL to clipboard
     408         */
     409        $('#prodshow-copy-redirect-url').on('click', function(e) {
     410            e.preventDefault();
     411           
     412            var $btn = $(this);
     413            var redirectUrl = $('#prodshow-redirect-url').text();
     414           
     415            // Copy to clipboard
     416            if (navigator.clipboard && navigator.clipboard.writeText) {
     417                navigator.clipboard.writeText(redirectUrl).then(function() {
     418                    showCopySuccess($btn);
     419                }).catch(function() {
     420                    fallbackCopy(redirectUrl, $btn);
     421                });
     422            } else {
     423                fallbackCopy(redirectUrl, $btn);
     424            }
     425        });
     426
     427        function fallbackCopy(text, $btn) {
     428            var $temp = $('<textarea>');
     429            $('body').append($temp);
     430            $temp.val(text).select();
     431            try {
     432                document.execCommand('copy');
     433                showCopySuccess($btn);
     434            } catch (err) {
     435                alert('Failed to copy. Please copy manually.');
     436            }
     437            $temp.remove();
     438        }
     439
     440        function showCopySuccess($btn) {
     441            var $icon = $btn.find('.dashicons');
     442            $icon.removeClass('dashicons-clipboard').addClass('dashicons-yes');
     443            $btn.addClass('copied');
     444           
     445            setTimeout(function() {
     446                $icon.removeClass('dashicons-yes').addClass('dashicons-clipboard');
     447                $btn.removeClass('copied');
     448            }, 2000);
     449        }
     450
     451        /**
     452         * Refresh API Version
     453         */
     454        $('#prodshow-refresh-api-version').on('click', function(e) {
     455            e.preventDefault();
     456           
     457            var $btn = $(this);
     458            var $icon = $btn.find('.dashicons');
     459            var nonce = $('#prodshow-api-version-nonce').val() || $('#prodshow-oauth-nonce').val() || $('#prodshow-clear-cache-nonce').val();
     460           
     461            // Show loading state
     462            $btn.prop('disabled', true);
     463            $icon.addClass('prodshow-spin');
     464           
     465            $.ajax({
     466                url: ajaxurl,
     467                type: 'POST',
     468                data: {
     469                    action: 'prodshow_refresh_api_version',
     470                    nonce: nonce
     471                },
     472                success: function(response) {
     473                    if (response.success) {
     474                        // Update the displayed version
     475                        var $display = $('.prodshow-api-version-display');
     476                        var newHtml = response.data.version +
     477                            ' <span class="prodshow-auto-detected">(' + 'auto-detected' + ')</span>' +
     478                            ' <button type="button" id="prodshow-refresh-api-version" class="button button-small" title="Refresh API Version">' +
     479                            '<span class="dashicons dashicons-update"></span></button>';
     480                        $display.html(newHtml);
     481                       
     482                        // Show success feedback
     483                        alert(response.data.message);
     484                       
     485                        // Reload to update all references
     486                        window.location.reload();
     487                    } else {
     488                        alert(response.data.message || 'Failed to refresh API version.');
     489                        $btn.prop('disabled', false);
     490                        $icon.removeClass('prodshow-spin');
     491                    }
     492                },
     493                error: function() {
     494                    alert('Connection error. Please try again.');
     495                    $btn.prop('disabled', false);
     496                    $icon.removeClass('prodshow-spin');
     497                }
     498            });
     499        });
     500
    287501    });
    288502
  • products-showcase/trunk/includes/class-admin-settings.php

    r3408553 r3435486  
    7777        );
    7878
     79        // OAuth settings
     80        register_setting(
     81            'prodshow_settings',
     82            'prodshow_shopify_client_id',
     83            array(
     84                'type' => 'string',
     85                'sanitize_callback' => 'sanitize_text_field',
     86                'default' => '',
     87                'show_in_rest' => false,
     88            )
     89        );
     90
     91        register_setting(
     92            'prodshow_settings',
     93            'prodshow_shopify_client_secret',
     94            array(
     95                'type' => 'string',
     96                'sanitize_callback' => 'sanitize_text_field',
     97                'default' => '',
     98                'show_in_rest' => false,
     99            )
     100        );
     101
    79102        register_setting(
    80103            'prodshow_settings',
     
    179202        }
    180203
     204        // Show OAuth success/error notices
     205        if (isset($_GET['oauth_success'])) {
     206            $success = get_transient('prodshow_oauth_success');
     207            if ($success) {
     208                delete_transient('prodshow_oauth_success');
     209                echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('🎉 Successfully connected to Shopify! Your store is now linked.', 'products-showcase') . '</p></div>';
     210            }
     211        }
     212
     213        if (isset($_GET['oauth_error'])) {
     214            $error = get_transient('prodshow_oauth_error');
     215            if ($error) {
     216                delete_transient('prodshow_oauth_error');
     217                echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__('Connection failed: ', 'products-showcase') . esc_html($error) . '</p></div>';
     218            }
     219        }
     220
    181221        // Test connection.
    182222        $connection_status = $this->test_shopify_connection();
     223        $is_oauth_connected = PRODSHOW_Shopify_OAuth::is_connected_via_oauth();
     224
     225        // Auto-detect API version if connected but not yet stored
     226        if ($connection_status['success']) {
     227            $stored_version = get_option('prodshow_shopify_api_version', '');
     228            if (empty($stored_version)) {
     229                // Try to detect and store the API version now
     230                PRODSHOW_Shopify_OAuth::refresh_api_version();
     231            }
     232        }
    183233
    184234        // Include the header.
     
    200250                                <div class="prodshow-section-header">
    201251                                    <h2><?php esc_html_e('🚀 Quick Start', 'products-showcase'); ?></h2>
    202                                     <p><?php esc_html_e('Connect your Shopify store and start displaying products:', 'products-showcase'); ?></p>
     252                                    <p><?php esc_html_e('Connect your Shopify store in 3 easy steps:', 'products-showcase'); ?></p>
    203253                                </div>
    204254                                <div class="prodshow-banner-content">
    205255                                    <ol class="prodshow-steps">
    206256                                        <li>
    207                                             <strong><?php esc_html_e('Create Custom App', 'products-showcase'); ?></strong>
    208                                             <p><?php esc_html_e('Shopify Admin → Settings → Apps → Develop apps → Create an app', 'products-showcase'); ?></p>
     257                                            <strong><?php esc_html_e('Create a Custom App in Shopify', 'products-showcase'); ?></strong>
     258                                            <p><?php esc_html_e('Go to Shopify Admin → Settings → Apps and sales channels → Develop apps → Create an app', 'products-showcase'); ?></p>
    209259                                        </li>
    210260                                        <li>
    211                                             <strong><?php esc_html_e('Enable API Access', 'products-showcase'); ?></strong>
    212                                             <p><?php esc_html_e('Configure Admin API → Enable "read_products" scope → Install app', 'products-showcase'); ?></p>
     261                                            <strong><?php esc_html_e('Configure API Access', 'products-showcase'); ?></strong>
     262                                            <p>
     263                                                <?php esc_html_e('In your app settings:', 'products-showcase'); ?><br>
     264                                                • <?php esc_html_e('Go to "Configuration" tab', 'products-showcase'); ?><br>
     265                                                • <?php esc_html_e('Under "Admin API integration", click "Configure"', 'products-showcase'); ?><br>
     266                                                • <?php esc_html_e('Enable "read_products" scope and save', 'products-showcase'); ?><br>
     267                                                • <?php
     268                                                    echo wp_kses(
     269                                                        sprintf(
     270                                                            /* translators: %s: redirect URL */
     271                                                            __('Set Allowed redirection URL to: %s', 'products-showcase'),
     272                                                            '<code>' . esc_html(PRODSHOW_Shopify_OAuth::get_redirect_uri()) . '</code>'
     273                                                        ),
     274                                                        array('code' => array())
     275                                                    );
     276                                                ?>
     277                                            </p>
    213278                                        </li>
    214279                                        <li>
    215                                             <strong><?php esc_html_e('Enter Credentials', 'products-showcase'); ?></strong>
    216                                             <p><?php esc_html_e('Copy your store URL and access token, paste below, and save', 'products-showcase'); ?></p>
    217                                         </li>
    218                                         <li>
    219                                             <strong><?php esc_html_e('Add Products to Your Site', 'products-showcase'); ?></strong>
    220                                             <p><?php esc_html_e('Edit any page/post → Click + to add block → Search "Shopify Product Showcase" → Select product or collection', 'products-showcase'); ?></p>
     280                                            <strong><?php esc_html_e('Copy Client ID & Secret', 'products-showcase'); ?></strong>
     281                                            <p><?php esc_html_e('Go to "API credentials" tab → Copy the Client ID and Client secret → Paste them below and click "Connect to Shopify"', 'products-showcase'); ?></p>
    221282                                        </li>
    222283                                    </ol>
    223284                                    <div class="prodshow-banner-links">
    224                                         <a href="https://shopify.dev/docs/apps/auth/admin-app-access-tokens" target="_blank" rel="noopener noreferrer" class="prodshow-link-primary">
     285                                        <a href="https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/" target="_blank" rel="noopener noreferrer" class="prodshow-link-primary">
    225286                                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    226287                                                <circle cx="12" cy="12" r="10"></circle>
     
    264325                                        <div class="prodshow-status-item">
    265326                                            <span class="prodshow-status-label"><?php esc_html_e('API Version:', 'products-showcase'); ?></span>
    266                                             <span class="prodshow-status-value"><?php echo esc_html(PRODSHOW_SHOPIFY_API_VERSION); ?></span>
     327                                            <span class="prodshow-status-value prodshow-api-version-display">
     328                                                <?php
     329                                                $current_api_version = prodshow_get_api_version();
     330                                                $stored_version = get_option('prodshow_shopify_api_version', '');
     331                                                echo esc_html($current_api_version);
     332                                                if (!empty($stored_version)) {
     333                                                    echo ' <span class="prodshow-auto-detected">(' . esc_html__('auto-detected', 'products-showcase') . ')</span>';
     334                                                } else {
     335                                                    echo ' <span class="prodshow-fallback-version">(' . esc_html__('fallback', 'products-showcase') . ')</span>';
     336                                                }
     337                                                ?>
     338                                                <button type="button" id="prodshow-refresh-api-version" class="button button-small" title="<?php esc_attr_e('Refresh API Version', 'products-showcase'); ?>">
     339                                                    <span class="dashicons dashicons-update"></span>
     340                                                </button>
     341                                                <input type="hidden" id="prodshow-api-version-nonce" value="<?php echo esc_attr(wp_create_nonce('prodshow_oauth_nonce')); ?>">
     342                                            </span>
    267343                                        </div>
    268344                                    </div>
     
    300376                        <?php settings_fields('prodshow_settings'); ?>
    301377
    302                         <!-- Shopify API Configuration Section -->
    303                         <div class="prodshow-section">
     378                        <!-- Shopify Connection Section (OAuth) -->
     379                        <div class="prodshow-section" id="prodshow-oauth-section">
    304380                            <div class="prodshow-section-header">
    305                                 <h2><?php esc_html_e('Shopify API Configuration', 'products-showcase'); ?></h2>
    306                                 <p><?php esc_html_e('Connect your WordPress site to your Shopify store by providing the necessary API credentials.', 'products-showcase'); ?></p>
     381                                <h2><?php esc_html_e('Connect to Shopify', 'products-showcase'); ?></h2>
     382                                <p><?php esc_html_e('Connect your WordPress site to your Shopify store using secure OAuth authentication. Just enter your app credentials and click connect!', 'products-showcase'); ?></p>
    307383                            </div>
    308384
    309                             <table class="form-table" role="presentation">
    310                                 <tbody>
    311                                     <tr>
    312                                         <th scope="row">
    313                                             <label for="prodshow_shopify_url"><?php esc_html_e('Shopify Store URL', 'products-showcase'); ?></label>
    314                                         </th>
    315                                         <td>
    316                                             <input type="text"
    317                                                      id="prodshow_shopify_url"
    318                                                      name="prodshow_shopify_url"
    319                                                      value="<?php echo esc_attr(get_option('prodshow_shopify_url')); ?>"
    320                                                      class="regular-text"
    321                                                      placeholder="your-store.myshopify.com">
    322                                             <p class="description">
    323                                                 <?php esc_html_e('Your Shopify store URL (without https://). Example: my-store.myshopify.com', 'products-showcase'); ?>
    324                                             </p>
    325                                         </td>
    326                                     </tr>
    327 
    328                                     <tr>
    329                                         <th scope="row">
    330                                             <label for="prodshow_shopify_access_token"><?php esc_html_e('Admin API Access Token', 'products-showcase'); ?></label>
    331                                         </th>
    332                                         <td>
    333                                             <input type="password"
    334                                                      id="prodshow_shopify_access_token"
    335                                                      name="prodshow_shopify_access_token"
    336                                                      value="<?php echo esc_attr(get_option('prodshow_shopify_access_token')); ?>"
    337                                                      class="regular-text"
    338                                                      autocomplete="off">
    339                                             <p class="description">
    340                                                 <?php
    341                                                 echo wp_kses(
    342                                                     __('Generate from <strong>Shopify Admin → Settings → Apps and sales channels → Develop apps</strong>. Required scopes: <code>read_products</code>', 'products-showcase'),
    343                                                     array(
    344                                                         'strong' => array(),
    345                                                         'code' => array(),
    346                                                     )
    347                                                 );
    348                                                 ?>
    349                                                 <br>
    350                                                 <a href="https://shopify.dev/docs/apps/auth/admin-app-access-tokens" target="_blank" rel="noopener noreferrer">
    351                                                     <?php esc_html_e('Learn how to create an access token →', 'products-showcase'); ?>
    352                                                 </a>
    353                                             </p>
    354                                         </td>
    355                                     </tr>
    356                                 </tbody>
    357                             </table>
     385                            <?php if ($is_oauth_connected && $connection_status['success']): ?>
     386                                <!-- Already connected via OAuth - show minimal info -->
     387                                <div class="prodshow-oauth-connected-info">
     388                                    <p class="prodshow-oauth-status">
     389                                        <span class="dashicons dashicons-yes-alt" style="color: #28a745;"></span>
     390                                        <?php esc_html_e('Connected via OAuth', 'products-showcase'); ?>
     391                                    </p>
     392                                    <button type="button" id="prodshow-disconnect-btn" class="button button-secondary">
     393                                        <?php esc_html_e('Disconnect', 'products-showcase'); ?>
     394                                    </button>
     395                                    <input type="hidden" id="prodshow-oauth-nonce" value="<?php echo esc_attr(wp_create_nonce('prodshow_oauth_nonce')); ?>">
     396                                    <!-- Preserve OAuth credentials when saving other settings -->
     397                                    <input type="hidden" name="prodshow_shopify_url" value="<?php echo esc_attr(get_option('prodshow_shopify_url')); ?>">
     398                                    <input type="hidden" name="prodshow_shopify_client_id" value="<?php echo esc_attr(get_option('prodshow_shopify_client_id')); ?>">
     399                                    <input type="hidden" name="prodshow_shopify_client_secret" value="<?php echo esc_attr(get_option('prodshow_shopify_client_secret')); ?>">
     400                                    <input type="hidden" name="prodshow_shopify_access_token" value="<?php echo esc_attr(get_option('prodshow_shopify_access_token')); ?>">
     401                                </div>
     402                            <?php else: ?>
     403                                <table class="form-table" role="presentation">
     404                                    <tbody>
     405                                        <tr>
     406                                            <th scope="row">
     407                                                <label for="prodshow_shopify_url"><?php esc_html_e('Shopify Store URL', 'products-showcase'); ?> <span class="required">*</span></label>
     408                                            </th>
     409                                            <td>
     410                                                <input type="text"
     411                                                         id="prodshow_shopify_url"
     412                                                         name="prodshow_shopify_url"
     413                                                         value="<?php echo esc_attr(get_option('prodshow_shopify_url')); ?>"
     414                                                         class="regular-text"
     415                                                         placeholder="your-store.myshopify.com"
     416                                                         required>
     417                                                <p class="description">
     418                                                    <?php esc_html_e('Your Shopify store URL (without https://). Example: my-store.myshopify.com', 'products-showcase'); ?>
     419                                                </p>
     420                                            </td>
     421                                        </tr>
     422
     423                                        <tr>
     424                                            <th scope="row">
     425                                                <label for="prodshow_shopify_client_id"><?php esc_html_e('Client ID', 'products-showcase'); ?> <span class="required">*</span></label>
     426                                            </th>
     427                                            <td>
     428                                                <input type="text"
     429                                                         id="prodshow_shopify_client_id"
     430                                                         name="prodshow_shopify_client_id"
     431                                                         value="<?php echo esc_attr(get_option('prodshow_shopify_client_id')); ?>"
     432                                                         class="regular-text"
     433                                                         placeholder="e.g., 1234567890abcdef..."
     434                                                         autocomplete="off"
     435                                                         required>
     436                                                <p class="description">
     437                                                    <?php esc_html_e('Found in your Shopify app settings under "API credentials" → "Client ID"', 'products-showcase'); ?>
     438                                                </p>
     439                                            </td>
     440                                        </tr>
     441
     442                                        <tr>
     443                                            <th scope="row">
     444                                                <label for="prodshow_shopify_client_secret"><?php esc_html_e('Client Secret', 'products-showcase'); ?> <span class="required">*</span></label>
     445                                            </th>
     446                                            <td>
     447                                                <input type="password"
     448                                                         id="prodshow_shopify_client_secret"
     449                                                         name="prodshow_shopify_client_secret"
     450                                                         value="<?php echo esc_attr(get_option('prodshow_shopify_client_secret')); ?>"
     451                                                         class="regular-text"
     452                                                         placeholder="e.g., shpss_..."
     453                                                         autocomplete="off"
     454                                                         required>
     455                                                <p class="description">
     456                                                    <?php esc_html_e('Found in your Shopify app settings under "API credentials" → "Client secret"', 'products-showcase'); ?>
     457                                                </p>
     458                                            </td>
     459                                        </tr>
     460
     461                                        <tr>
     462                                            <th scope="row">
     463                                                <label><?php esc_html_e('Redirect URL', 'products-showcase'); ?></label>
     464                                            </th>
     465                                            <td>
     466                                                <div class="prodshow-redirect-url-box">
     467                                                    <code id="prodshow-redirect-url"><?php echo esc_html(PRODSHOW_Shopify_OAuth::get_redirect_uri()); ?></code>
     468                                                    <button type="button" id="prodshow-copy-redirect-url" class="button button-small" title="<?php esc_attr_e('Copy to clipboard', 'products-showcase'); ?>">
     469                                                        <span class="dashicons dashicons-clipboard"></span>
     470                                                    </button>
     471                                                </div>
     472                                                <p class="description">
     473                                                    <?php esc_html_e('Copy this URL and paste it in your Shopify app\'s "Allowed redirection URL(s)" field.', 'products-showcase'); ?>
     474                                                </p>
     475                                            </td>
     476                                        </tr>
     477                                    </tbody>
     478                                </table>
     479
     480                                <div class="prodshow-oauth-actions">
     481                                    <button type="button" id="prodshow-connect-btn" class="button button-primary button-large prodshow-connect-button">
     482                                        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
     483                                            <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
     484                                            <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
     485                                        </svg>
     486                                        <?php esc_html_e('Connect to Shopify', 'products-showcase'); ?>
     487                                    </button>
     488                                    <span class="prodshow-oauth-spinner spinner"></span>
     489                                </div>
     490
     491                                <input type="hidden" id="prodshow-oauth-nonce" value="<?php echo esc_attr(wp_create_nonce('prodshow_oauth_nonce')); ?>">
     492                            <?php endif; ?>
    358493                        </div>
    359494
  • products-showcase/trunk/products-showcase.php

    r3408553 r3435486  
    44 * Plugin URI: https://github.com/HosseinKarami/products-showcase
    55 * Description: Display Shopify products and collections in beautiful carousels using native Gutenberg blocks. Features product filtering, color swatches, and responsive design.
    6  * Version: 1.0.0
     6 * Version: 1.1.0
    77 * Requires at least: 6.0
    88 * Requires PHP: 8.1
     
    2323
    2424// Plugin constants.
    25 define( 'PRODSHOW_VERSION', '1.0.0' );
     25define( 'PRODSHOW_VERSION', '1.1.0' );
    2626define( 'PRODSHOW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2727define( 'PRODSHOW_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    2828define( 'PRODSHOW_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
    29 define( 'PRODSHOW_SHOPIFY_API_VERSION', '2025-10' ); // Shopify Admin API version.
     29define( 'PRODSHOW_SHOPIFY_API_VERSION_FALLBACK', '2025-10' ); // Fallback Shopify Admin API version.
     30
     31/**
     32 * Get the Shopify API version to use
     33 *
     34 * Returns the dynamically detected version if available, otherwise falls back to default.
     35 *
     36 * @return string The API version to use (e.g., '2025-10')
     37 */
     38function prodshow_get_api_version() {
     39    $stored_version = get_option( 'prodshow_shopify_api_version', '' );
     40   
     41    if ( ! empty( $stored_version ) ) {
     42        return $stored_version;
     43    }
     44   
     45    return PRODSHOW_SHOPIFY_API_VERSION_FALLBACK;
     46}
     47
     48// For backwards compatibility, define the constant using the dynamic function
     49// Note: This runs early, so it will use the stored value or fallback
     50if ( ! defined( 'PRODSHOW_SHOPIFY_API_VERSION' ) ) {
     51    // We can't call prodshow_get_api_version() before plugins_loaded in some cases,
     52    // so we check the option directly here
     53    $_prodshow_api_version = get_option( 'prodshow_shopify_api_version', '' );
     54    if ( empty( $_prodshow_api_version ) ) {
     55        $_prodshow_api_version = PRODSHOW_SHOPIFY_API_VERSION_FALLBACK;
     56    }
     57    define( 'PRODSHOW_SHOPIFY_API_VERSION', $_prodshow_api_version );
     58    unset( $_prodshow_api_version );
     59}
    3060
    3161/**
     
    3767    require_once PRODSHOW_PLUGIN_DIR . 'includes/class-shopify-block.php';
    3868    require_once PRODSHOW_PLUGIN_DIR . 'includes/class-shopify-graphql-types.php';
     69    require_once PRODSHOW_PLUGIN_DIR . 'includes/class-shopify-oauth.php';
    3970    require_once PRODSHOW_PLUGIN_DIR . 'includes/class-admin-settings.php';
    4071    require_once PRODSHOW_PLUGIN_DIR . 'includes/class-enqueue-assets.php';
     
    4576    new PRODSHOW_Shopify_Block();
    4677    new PRODSHOW_Shopify_GraphQL_Types();
     78    new PRODSHOW_Shopify_OAuth();
    4779    new PRODSHOW_Admin_Settings();
    4880    new PRODSHOW_Enqueue_Assets();
    4981    new PRODSHOW_REST_API();
     82
     83    // Auto-detect API version if connected but no version stored yet
     84    add_action( 'admin_init', 'prodshow_maybe_detect_api_version' );
    5085}
    5186add_action( 'plugins_loaded', 'prodshow_init' );
     87
     88/**
     89 * Detect and store API version if connected but not yet detected
     90 *
     91 * This runs once when an existing connection doesn't have a stored API version.
     92 */
     93function prodshow_maybe_detect_api_version() {
     94    // Only run once per day to avoid excessive API calls
     95    $last_check = get_transient( 'prodshow_api_version_check' );
     96    if ( $last_check ) {
     97        return;
     98    }
     99
     100    // Check if we have a connection but no stored API version
     101    $shop_url = get_option( 'prodshow_shopify_url', '' );
     102    $access_token = get_option( 'prodshow_shopify_access_token', '' );
     103    $stored_version = get_option( 'prodshow_shopify_api_version', '' );
     104
     105    // If we have credentials but no stored version, try to detect it
     106    if ( ! empty( $shop_url ) && ! empty( $access_token ) && empty( $stored_version ) ) {
     107        $detected_version = PRODSHOW_Shopify_OAuth::get_latest_api_version( $shop_url, $access_token );
     108       
     109        if ( $detected_version ) {
     110            update_option( 'prodshow_shopify_api_version', $detected_version );
     111        }
     112    }
     113
     114    // Set transient to prevent checking again for 24 hours
     115    set_transient( 'prodshow_api_version_check', true, DAY_IN_SECONDS );
     116}
    52117
    53118/**
  • products-showcase/trunk/readme.txt

    r3408830 r3435486  
    11=== Products Showcase – Shopify Integration ===
    22Contributors: hosseinkarami
    3 Donate link: https://buymeacoffee.com/hosseinkarami?utm_source=wordpress&utm_medium=readme&utm_campaign=products-showcase&utm_content=readme-donate
    43Tags: shopify, ecommerce, products, gutenberg, blocks
    54Requires at least: 6.0
    65Tested up to: 6.9
    7 Stable tag: 1.0.0
     6Stable tag: 1.1.0
    87Requires PHP: 8.1
    98License: GPLv2 or later
     
    2221
    2322To build from source:
    24 `
    25     npm install
    26     npm run build
    27 `
     23```
     24npm install
     25npm run build
     26```
    2827
    2928For development with hot reloading:
    30 `
    31     npm start
    32 `
     29```
     30npm start
     31```
    3332
    3433== Description ==
     
    8281**Data Transmitted to Shopify**:
    83821. **Shopify Store URL** - Your store's domain (e.g., `your-store.myshopify.com`) configured in plugin settings
    84 2. **Admin API Access Token** - Your Shopify Admin API authentication token (configured in plugin settings)
    85 3. **GraphQL Queries** - Specific queries requesting product and collection data
     832. **OAuth Credentials** - Client ID and Client Secret are used during the one-time OAuth authorization flow
     843. **Admin API Access Token** - Obtained automatically via OAuth and used for all subsequent API requests
     854. **GraphQL Queries** - Specific queries requesting product and collection data
    8686
    8787**When Data is Transmitted**:
     
    1461465. Activate the plugin in WordPress
    147147
    148 = Getting Your Shopify API Credentials =
     148= Creating Your Shopify App =
    149149
    1501501. Log in to your Shopify Admin
    1511512. Go to Settings → Apps and sales channels
    1521523. Click "Develop apps"
    153 4. Create a new app or select an existing one
    154 5. Configure Admin API scopes (required: `read_products`)
    155 6. Install the app to your store
    156 7. Copy the Admin API access token
    157 8. Paste it into the plugin settings at Settings → Shopify Products
     1534. Click "Create an app" and name it (e.g., "WordPress Integration")
     1545. Go to the "Configuration" tab
     1556. Under "Admin API integration", click "Configure"
     1567. Enable the `read_products` scope and save
     1578. Under "Allowed redirection URL(s)", add the Redirect URL shown in your WordPress plugin settings
     1589. Go to the "API credentials" tab
     15910. Copy the **Client ID** and **Client secret**
    158160
    159161== Configuration ==
    160162
    161 = Plugin Settings =
    162 
    163 1. Navigate to **Settings → Shopify Products** in WordPress admin
     163= Easy OAuth Setup =
     164
     1651. Navigate to **Shopify Products** in WordPress admin
    1641662. Enter your **Shopify Store URL** (e.g., `your-store.myshopify.com`)
    165 3. Paste your **Admin API Access Token**
    166 4. Set **Cache Duration** (default: 1 hour, recommended to reduce API calls)
    167 5. Click **Save Settings**
    168 
    169 The plugin will automatically test the connection and display a success message if configured correctly.
     1673. Paste your **Client ID** from Shopify
     1684. Paste your **Client Secret** from Shopify
     1695. Click **"Connect to Shopify"**
     1706. You'll be redirected to Shopify to authorize the connection
     1717. After authorizing, you're automatically redirected back - done!
     172
     173The plugin automatically obtains the access token via secure OAuth and detects the latest Shopify API version.
    170174
    171175== Usage ==
     
    218222
    219223Target these CSS classes for custom styling:
    220 `
    221     /* Container */
    222     .wp-block-products-showcase-products { }
    223     .prodshow-shopify-block { }
    224     .prodshow-container { }
    225 
    226     /* Header */
    227     .prodshow-title { }
    228     .prodshow-description { }
    229     .prodshow-cta-button { }
    230 
    231     /* Carousel */
    232     .prodshow-carousel { }
    233     .prodshow-carousel-viewport { }
    234     .prodshow-carousel-container { }
    235     .prodshow-carousel-btn { }
    236 
    237     /* Product Cards */
    238     .prodshow-product-card { }
    239     .prodshow-product-image { }
    240     .prodshow-product-title { }
    241     .prodshow-product-price { }
    242     .prodshow-product-swatches { }
    243     .prodshow-swatch { }
    244 
    245     /* Single Product Layout */
    246     .prodshow-single-product { }
    247     .prodshow-single-info { }
    248 `
     224
     225```css
     226/* Container */
     227.wp-block-products-showcase-products { }
     228.prodshow-shopify-block { }
     229.prodshow-container { }
     230
     231/* Header */
     232.prodshow-title { }
     233.prodshow-description { }
     234.prodshow-cta-button { }
     235
     236/* Carousel */
     237.prodshow-carousel { }
     238.prodshow-carousel-viewport { }
     239.prodshow-carousel-container { }
     240.prodshow-carousel-btn { }
     241
     242/* Product Cards */
     243.prodshow-product-card { }
     244.prodshow-product-image { }
     245.prodshow-product-title { }
     246.prodshow-product-price { }
     247.prodshow-product-swatches { }
     248.prodshow-swatch { }
     249
     250/* Single Product Layout */
     251.prodshow-single-product { }
     252.prodshow-single-info { }
     253```
    249254
    250255= Template Overrides =
    251256
    252257Copy templates to your theme for customization:
    253 `
    254     your-theme/
    255       products-showcase/
    256         block-template.php
    257         product-card.php
    258 `
     258
     259```
     260your-theme/
     261  products-showcase/
     262    block-template.php
     263    product-card.php
     264```
    259265
    260266The plugin will automatically use your theme templates if they exist.
     
    275281
    276282Modify cache duration:
    277 `
    278     add_filter('prodshow_cache_duration', function($duration) {
    279         return 2 * HOUR_IN_SECONDS;
    280     });
    281 `
     283```php
     284add_filter('prodshow_cache_duration', function($duration) {
     285    return 2 * HOUR_IN_SECONDS;
     286});
     287```
    282288
    283289Customize product data before display:
    284 `
    285     add_filter('prodshow_product_data', function($product) {
    286         // Modify product data
    287         return $product;
    288     }, 10, 1);
    289 `
     290```php
     291add_filter('prodshow_product_data', function($product) {
     292    // Modify product data
     293    return $product;
     294}, 10, 1);
     295```
    290296
    291297Add custom product filtering:
    292 `
    293     add_filter('prodshow_filter_products', function($products) {
    294         // Filter products array
    295         return $products;
    296     }, 10, 1);
    297 `
     298```php
     299add_filter('prodshow_filter_products', function($products) {
     300    // Filter products array
     301    return $products;
     302}, 10, 1);
     303```
    298304
    299305= Programmatic Usage =
    300 `
    301     // Get Shopify API instance
    302     $shopify_api = new PRODSHOW_Shopify_API();
    303 
    304     // Search products
    305     $products = $shopify_api->search_products('shirt');
    306 
    307     // Search collections
    308     $collections = $shopify_api->search_collections('summer');
    309 
    310     // Fetch product data
    311     $product = $shopify_api->fetch_product_data('gid://shopify/Product/123456');
    312 
    313     // Fetch collection products 
    314     $products = $shopify_api->fetch_collection_products('gid://shopify/Collection/789', 12);
    315 `
     306
     307```php
     308// Get Shopify API instance
     309$shopify_api = new PRODSHOW_Shopify_API();
     310
     311// Search products
     312$products = $shopify_api->search_products('shirt');
     313
     314// Search collections
     315$collections = $shopify_api->search_collections('summer');
     316
     317// Fetch product data
     318$product = $shopify_api->fetch_product_data('gid://shopify/Product/123456');
     319
     320// Fetch collection products 
     321$products = $shopify_api->fetch_collection_products('gid://shopify/Collection/789', 12);
     322```
    316323
    317324= Build Scripts =
    318 `
    319     # Development - watch for changes
    320     npm start
    321 
    322     # Production build
    323     npm run build
    324 
    325     # Linting
    326     npm run lint:js        # Lint JavaScript
    327     npm run lint:css       # Lint styles
    328     npm run lint:pkg-json  # Lint package.json
    329 
    330     # Formatting
    331     npm run format         # Auto-fix code style
    332 
    333     # Create plugin zip
    334     npm run plugin-zip
    335 `   
     325
     326```bash
     327# Development - watch for changes
     328npm start
     329
     330# Production build
     331npm run build
     332
     333# Linting
     334npm run lint:js        # Lint JavaScript
     335npm run lint:css       # Lint styles
     336npm run lint:pkg-json  # Lint package.json
     337
     338# Formatting
     339npm run format         # Auto-fix code style
     340
     341# Create plugin zip
     342npm run plugin-zip
     343```
    336344
    337345== Frequently Asked Questions ==
     
    367375= What happens if my Shopify API credentials change? =
    368376
    369 Simply update the credentials in Settings → Shopify Products. The plugin will automatically use the new credentials for all future API requests.
     377Click the "Disconnect" button in Shopify Products settings, then reconnect using the OAuth flow with your new credentials. The plugin will automatically obtain a new access token.
    370378
    371379= Can I filter out certain products? =
     
    4014091. Ensure Node.js 18+ is installed
    4024102. Clean install:
    403 `
    404     rm -rf node_modules package-lock.json
    405     npm install
    406     npm run build
    407 `
    408 
     411   ```
     412   rm -rf node_modules package-lock.json
     413   npm install
     414   npm run build
     415   ```
    4094163. Check @wordpress/scripts version compatibility
    410417
     
    419426
    420427== Changelog ==
     428
     429= 1.1.0 =
     430* **NEW: OAuth 2.0 Authentication** - Easy one-click connection to Shopify using secure OAuth flow
     431* **NEW: Auto API Version Detection** - Automatically detects and uses the latest Shopify API version
     432* **NEW: Simplified Setup** - No more manual access token copying - just enter Client ID & Secret and click Connect
     433* **NEW: Disconnect/Reconnect** - Easy way to change Shopify credentials
     434* **NEW: Refresh API Version** - Button to manually refresh the detected API version
     435* Improved admin UI with better connection status display
     436* Enhanced security with OAuth state validation
    421437
    422438= 1.0.0 =
     
    437453== Upgrade Notice ==
    438454
     455= 1.1.0 =
     456Major update! Now featuring OAuth 2.0 authentication - no more manual token copying. Just enter your Client ID & Secret and click Connect. Existing connections will continue to work.
     457
    439458= 1.0.0 =
    440459Initial release of Products Showcase – Shopify Integration. Display your Shopify products beautifully on WordPress!
Note: See TracChangeset for help on using the changeset viewer.