# ShopRenter Integration Development Plan **Project:** ShopCall.ai - ShopRenter Integration **Platform:** ShopRenter (Hungarian e-commerce platform) **Integration Type:** OAuth 2.0 App with API Access **Documentation Source:** https://doc.shoprenter.hu/development/app-development/ --- ## πŸ“‹ Table of Contents 1. [Overview](#overview) 2. [Registration Requirements](#registration-requirements) 3. [Technical Architecture](#technical-architecture) 4. [OAuth Flow Implementation](#oauth-flow-implementation) 5. [API Integration](#api-integration) 6. [Database Schema](#database-schema) 7. [Backend Implementation](#backend-implementation) 8. [Frontend Implementation](#frontend-implementation) 9. [Security Considerations](#security-considerations) 10. [Testing Strategy](#testing-strategy) 11. [Deployment Plan](#deployment-plan) 12. [Timeline & Milestones](#timeline--milestones) --- ## 🎯 Overview ### What is ShopRenter? ShopRenter is a Hungarian e-commerce platform (similar to Shopify) that provides online store solutions. This integration will enable ShopCall.ai to connect with ShopRenter stores and provide AI-powered calling services. ### Integration Goals - βœ… Enable ShopRenter merchants to connect their stores via OAuth - βœ… Sync product catalog, customer data, and order information - βœ… Provide AI calling services for ShopRenter merchants - βœ… Support Hungarian language and HUF currency - βœ… Comply with ShopRenter's app requirements and guidelines ### Key Features 1. **OAuth 2.0 Authentication** - Secure store connection 2. **Product Sync** - Automatic product catalog synchronization 3. **Customer Data Access** - Customer information for personalized calls 4. **Order Information** - Access to order status for customer inquiries 5. **Webhook Support** - Real-time updates for orders and products --- ## πŸ“ Registration Requirements ### Data Required to Register with ShopRenter **Must be sent to:** partnersupport@shoprenter.hu #### 1. Application Information **Application Name:** ``` ShopCall.ai - AI Phone Assistant ``` **Short Description (max 70 chars):** ``` AI-powered phone assistant for automated customer service calls ``` **Application Details Link:** ``` https://shopcall.ai ``` **Application Type:** ``` Redirected (user redirected to our platform, not iframe) ``` #### 2. Technical Endpoints **⚠️ Note:** The URLs below are examples. These are now configurable via environment variables: - `FRONTEND_URL` - for EntryPoint - `BACKEND_URL` - for RedirectUri and UninstallUri See `.env.example` files for configuration details. **EntryPoint (HTTPS required):** ``` https://shopcall.ai/integrations/shoprenter/entry ``` - This is where users land after OAuth installation - Must validate HMAC on entry - Displays integration success/configuration page **RedirectUri (HTTPS required):** ``` https://shopcall-ai-backend.vercel.app/auth/shoprenter/callback ``` - OAuth callback endpoint - Receives: shopname, code, timestamp, hmac, app_url - Validates HMAC and exchanges code for tokens **UninstallUri (HTTPS required):** ``` https://shopcall-ai-backend.vercel.app/webhooks/shoprenter/uninstall ``` - Called when app is uninstalled - Receives: shopname, code, timestamp, hmac - Cleanup: remove tokens, deactivate services #### 3. Visual Assets **Application Logo:** - Dimensions: **250x150px** - Format: PNG with transparency - Location: `/shopcall.ai-main/public/images/shoprenter-app-logo.png` #### 4. Test Store **Test Store Name:** ``` shopcall-test-store ``` - URL will be: `shopcall-test-store.myshoprenter.hu` - Request at: https://www.shoprenter.hu/tesztigenyles/?devstore=1 #### 5. Required Scopes Based on ShopCall.ai functionality, we need: ``` product:read # Read product catalog product:write # Update product information (if needed) customer:read # Access customer data customer:write # Update customer information (notes, tags) order:read # Read order information order:write # Update order status, add notes category:read # Read product categories webhook:read # List webhooks webhook:write # Create/update webhooks for real-time sync ``` **Scope Justification:** - **product:read** - Sync product catalog for AI knowledge base - **customer:read** - Personalize AI responses based on customer history - **order:read** - Provide order status information during calls - **order:write** - Update order notes after calls - **webhook:write** - Set up real-time synchronization --- ## πŸ—οΈ Technical Architecture ### System Overview ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ShopRenter β”‚ β”‚ ShopCall.ai β”‚ β”‚ Supabase β”‚ β”‚ Store │◄───────►│ Backend │◄───────►│ Database β”‚ β”‚ β”‚ OAuth β”‚ β”‚ Store β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ Webhooks β”‚ API Calls β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Product Sync β”‚ β”‚ AI Phone β”‚ β”‚ Order Updates β”‚ β”‚ Assistant β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### Component Breakdown **Frontend (React/Vite):** - ShopRenter connection button - OAuth initiation - Store configuration UI - Success/error handling **Backend (Express.js/Node.js):** - OAuth flow handling - HMAC validation - Token management - API client for ShopRenter - Webhook receivers **Database (Supabase):** - Store credentials storage - Product catalog cache - Sync status tracking --- ## πŸ” OAuth Flow Implementation ### Installation Process Flow ```mermaid sequenceDiagram participant Merchant participant ShopRenter participant ShopCall Backend participant Database Merchant->>ShopRenter: Click "Install ShopCall.ai" ShopRenter->>ShopCall Backend: GET /auth/shoprenter/callback?shopname=X&code=Y×tamp=Z&hmac=H&app_url=U ShopCall Backend->>ShopCall Backend: Validate HMAC (sha256) alt HMAC Valid ShopCall Backend->>Database: Store installation data ShopCall Backend->>Merchant: Redirect to app_url ShopRenter->>ShopCall Backend: GET /integrations/shoprenter/entry?shopname=X&code=Y×tamp=Z&hmac=H ShopCall Backend->>ShopCall Backend: Validate HMAC again ShopCall Backend->>ShopCall Backend: Request OAuth token from ShopRenter ShopCall Backend->>Database: Store access token ShopCall Backend->>Merchant: Show success page else HMAC Invalid ShopCall Backend->>Merchant: Show error (403) end ``` ### HMAC Validation Process **ShopRenter sends:** ``` GET /auth/shoprenter/callback?shopname=example&code=0907a61c0c8d55e99db179b68161bc00×tamp=1337178173&hmac=d48e5d...&app_url=... ``` **Validation Algorithm:** ```javascript function validateHMAC(query, clientSecret) { // 1. Extract HMAC from query const { hmac, ...params } = query; // 2. Build query string without HMAC (sorted alphabetically) const sortedParams = Object.keys(params) .sort() .map(key => `${key}=${params[key]}`) .join('&'); // Example: "code=0907a61c0c8d55e99db179b68161bc00&shopname=example×tamp=1337178173" // 3. Generate HMAC using sha256 const crypto = require('crypto'); const calculatedHmac = crypto .createHmac('sha256', clientSecret) .update(sortedParams) .digest('hex'); // 4. Compare (timing-safe) return crypto.timingSafeEqual( Buffer.from(calculatedHmac), Buffer.from(hmac) ); } ``` **Security Notes:** - ⚠️ **Always validate HMAC** before processing request - ⚠️ Use timing-safe comparison to prevent timing attacks - ⚠️ Check timestamp to prevent replay attacks (within 5 minutes) - ⚠️ Store ClientSecret securely in environment variables --- ## πŸ”Œ API Integration ### OAuth Token Acquisition After successful HMAC validation, request access token: **Endpoint:** (To be confirmed with ShopRenter Partner Support) ``` POST https://{shopname}.myshoprenter.hu/oauth/token ``` **Request Body:** ```json { "grant_type": "authorization_code", "client_id": "{ClientId}", "client_secret": "{ClientSecret}", "code": "{code_from_callback}", "redirect_uri": "https://shopcall-ai-backend.vercel.app/auth/shoprenter/callback" } ``` **Response:** ```json { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "def50200...", "scope": "product:read customer:read order:read" } ``` ### API Base URL ``` https://{shopname}.myshoprenter.hu/api ``` ### Common API Endpoints Based on ShopRenter API documentation: #### 1. Products **List Products:** ``` GET /api/products Authorization: Bearer {access_token} Response: { "items": [ { "id": "cHJvZHVjdC1wcm9kdWN0X2lkPTE=", "name": "Product Name", "sku": "SKU-123", "price": "9990.00", "currency": "HUF", "description": "Product description", "stock": 10, "active": true } ], "pagination": { "total": 150, "page": 1, "limit": 25 } } ``` #### 2. Customers **List Customers:** ``` GET /api/customers Authorization: Bearer {access_token} Response: { "items": [ { "id": "Y3VzdG9tZXItY3VzdG9tZXJfaWQ9MQ==", "firstname": "JΓ‘nos", "lastname": "KovΓ‘cs", "email": "janos.kovacs@example.com", "phone": "+36201234567", "created_at": "2024-01-15T10:30:00Z" } ] } ``` #### 3. Orders **List Orders:** ``` GET /api/orders Authorization: Bearer {access_token} Response: { "items": [ { "id": "b3JkZXItb3JkZXJfaWQ9MQ==", "order_number": "SR-2024-001", "customer_id": "Y3VzdG9tZXItY3VzdG9tZXJfaWQ9MQ==", "status": "processing", "total": "29990.00", "currency": "HUF", "created_at": "2024-01-20T14:25:00Z", "items": [...] } ] } ``` #### 4. Webhooks **Register Webhook:** ``` POST /api/webhooks Authorization: Bearer {access_token} Content-Type: application/json Body: { "event": "order/create", "address": "https://shopcall-ai-backend.vercel.app/webhooks/shoprenter/orders", "active": true } Response: { "id": "d2ViaG9vay13ZWJob29rX2lkPTE=", "event": "order/create", "address": "https://...", "active": true } ``` **Available Webhook Events:** - `order/create` - New order created - `order/update` - Order status changed - `product/create` - New product added - `product/update` - Product information changed - `product/delete` - Product deleted - `customer/create` - New customer registered - `customer/update` - Customer information updated --- ## πŸ’Ύ Database Schema ### New Table: `shoprenter_tokens` ```sql CREATE TABLE shoprenter_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), store_id UUID REFERENCES stores(id) ON DELETE CASCADE, -- OAuth tokens access_token TEXT NOT NULL, refresh_token TEXT, token_type VARCHAR(20) DEFAULT 'Bearer', expires_at TIMESTAMP WITH TIME ZONE, -- Scopes scopes TEXT[], -- ['product:read', 'customer:read', ...] -- Store information shopname VARCHAR(255) NOT NULL, -- e.g., 'example' from 'example.myshoprenter.hu' shop_domain VARCHAR(255) NOT NULL, -- Full domain -- Status is_active BOOLEAN DEFAULT true, last_sync_at TIMESTAMP WITH TIME ZONE, -- Metadata created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Constraints UNIQUE(store_id) ); -- Index for fast lookups CREATE INDEX idx_shoprenter_tokens_shopname ON shoprenter_tokens(shopname); CREATE INDEX idx_shoprenter_tokens_active ON shoprenter_tokens(is_active); -- Trigger for updated_at CREATE TRIGGER set_shoprenter_tokens_updated_at BEFORE UPDATE ON shoprenter_tokens FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ### Update Existing `stores` Table ```sql -- Add ShopRenter-specific columns ALTER TABLE stores ADD COLUMN IF NOT EXISTS shoprenter_app_id VARCHAR(50); ALTER TABLE stores ADD COLUMN IF NOT EXISTS shoprenter_client_id VARCHAR(255); ``` ### New Table: `shoprenter_products_cache` ```sql CREATE TABLE shoprenter_products_cache ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), store_id UUID REFERENCES stores(id) ON DELETE CASCADE, -- Product data from ShopRenter API shoprenter_product_id VARCHAR(255) NOT NULL, name VARCHAR(500), sku VARCHAR(255), price DECIMAL(10,2), currency VARCHAR(3) DEFAULT 'HUF', description TEXT, stock INTEGER, active BOOLEAN, -- Raw data raw_data JSONB, -- Full API response -- Sync metadata last_synced_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Constraints UNIQUE(store_id, shoprenter_product_id) ); -- Indexes CREATE INDEX idx_shoprenter_products_store ON shoprenter_products_cache(store_id); CREATE INDEX idx_shoprenter_products_sku ON shoprenter_products_cache(sku); CREATE INDEX idx_shoprenter_products_active ON shoprenter_products_cache(active); ``` ### New Table: `shoprenter_webhooks` ```sql CREATE TABLE shoprenter_webhooks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), store_id UUID REFERENCES stores(id) ON DELETE CASCADE, -- Webhook details shoprenter_webhook_id VARCHAR(255), event VARCHAR(100) NOT NULL, -- 'order/create', 'product/update', etc. callback_url TEXT NOT NULL, is_active BOOLEAN DEFAULT true, -- Statistics last_received_at TIMESTAMP WITH TIME ZONE, total_received INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(store_id, event) ); ``` --- ## πŸ”§ Backend Implementation ### File Structure ``` shopcall.ai-backend-main/ β”œβ”€β”€ api/ β”‚ └── index.js # Main server file β”œβ”€β”€ lib/ β”‚ └── shoprenter/ β”‚ β”œβ”€β”€ oauth.js # OAuth flow handling β”‚ β”œβ”€β”€ api-client.js # ShopRenter API client β”‚ β”œβ”€β”€ hmac-validator.js # HMAC validation utility β”‚ β”œβ”€β”€ webhook-processor.js # Webhook handling β”‚ └── sync-service.js # Product/customer sync └── config/ └── shoprenter.config.js # ShopRenter configuration ``` ### 1. Configuration (`config/shoprenter.config.js`) ```javascript module.exports = { appId: process.env.SHOPRENTER_APP_ID, clientId: process.env.SHOPRENTER_CLIENT_ID, clientSecret: process.env.SHOPRENTER_CLIENT_SECRET, redirectUri: process.env.NODE_ENV === 'production' ? 'https://shopcall-ai-backend.vercel.app/auth/shoprenter/callback' : 'http://localhost:3000/auth/shoprenter/callback', entryPoint: process.env.NODE_ENV === 'production' ? 'https://shopcall.ai/integrations/shoprenter/entry' : 'http://localhost:8080/integrations/shoprenter/entry', uninstallUri: process.env.NODE_ENV === 'production' ? 'https://shopcall-ai-backend.vercel.app/webhooks/shoprenter/uninstall' : 'http://localhost:3000/webhooks/shoprenter/uninstall', scopes: [ 'product:read', 'product:write', 'customer:read', 'customer:write', 'order:read', 'order:write', 'category:read', 'webhook:read', 'webhook:write' ], apiVersion: 'v1', maxRequestsPerSecond: 5, // Rate limiting tokenExpiryBuffer: 300, // Refresh 5 min before expiry }; ``` ### 2. HMAC Validator (`lib/shoprenter/hmac-validator.js`) ```javascript const crypto = require('crypto'); const config = require('../../config/shoprenter.config'); /** * Validate HMAC signature from ShopRenter * @param {Object} query - Query parameters from request * @param {string} query.hmac - HMAC signature * @param {string} query.shopname - Store name * @param {string} query.code - Authorization code * @param {string} query.timestamp - Request timestamp * @returns {boolean} - True if HMAC is valid */ function validateHMAC(query) { const { hmac, ...params } = query; if (!hmac) { console.error('[ShopRenter] HMAC missing from request'); return false; } // Build sorted query string without HMAC const sortedParams = Object.keys(params) .sort() .map(key => `${key}=${params[key]}`) .join('&'); // Calculate HMAC using sha256 const calculatedHmac = crypto .createHmac('sha256', config.clientSecret) .update(sortedParams) .digest('hex'); // Timing-safe comparison try { return crypto.timingSafeEqual( Buffer.from(calculatedHmac), Buffer.from(hmac) ); } catch (error) { console.error('[ShopRenter] HMAC comparison error:', error); return false; } } /** * Validate request timestamp (prevent replay attacks) * @param {string} timestamp - Unix timestamp from request * @param {number} maxAgeSeconds - Maximum age allowed (default: 300 = 5 min) * @returns {boolean} - True if timestamp is within valid range */ function validateTimestamp(timestamp, maxAgeSeconds = 300) { const requestTime = parseInt(timestamp, 10); const currentTime = Math.floor(Date.now() / 1000); const age = currentTime - requestTime; if (age < 0) { console.error('[ShopRenter] Request timestamp is in the future'); return false; } if (age > maxAgeSeconds) { console.error(`[ShopRenter] Request timestamp too old: ${age}s > ${maxAgeSeconds}s`); return false; } return true; } /** * Validate complete ShopRenter request * @param {Object} query - Query parameters * @returns {Object} - { valid: boolean, error?: string } */ function validateRequest(query) { // Check required parameters const required = ['shopname', 'code', 'timestamp', 'hmac']; const missing = required.filter(param => !query[param]); if (missing.length > 0) { return { valid: false, error: `Missing required parameters: ${missing.join(', ')}` }; } // Validate timestamp if (!validateTimestamp(query.timestamp)) { return { valid: false, error: 'Request timestamp invalid or expired' }; } // Validate HMAC if (!validateHMAC(query)) { return { valid: false, error: 'HMAC validation failed' }; } return { valid: true }; } module.exports = { validateHMAC, validateTimestamp, validateRequest }; ``` ### 3. OAuth Handler (`lib/shoprenter/oauth.js`) ```javascript const axios = require('axios'); const config = require('../../config/shoprenter.config'); const { createClient } = require('@supabase/supabase-js'); const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY ); /** * Exchange authorization code for access token * @param {string} shopname - Store name * @param {string} code - Authorization code * @returns {Promise} - Token response */ async function exchangeCodeForToken(shopname, code) { const tokenUrl = `https://${shopname}.myshoprenter.hu/oauth/token`; try { const response = await axios.post(tokenUrl, { grant_type: 'authorization_code', client_id: config.clientId, client_secret: config.clientSecret, code: code, redirect_uri: config.redirectUri }, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); console.log(`[ShopRenter] Token acquired for ${shopname}`); return response.data; } catch (error) { console.error('[ShopRenter] Token exchange error:', error.response?.data || error.message); throw new Error('Failed to exchange code for token'); } } /** * Store ShopRenter credentials in database * @param {string} userId - User ID * @param {string} shopname - Store name * @param {Object} tokenData - Token response from OAuth * @returns {Promise} - Database record */ async function storeCredentials(userId, shopname, tokenData) { const shopDomain = `${shopname}.myshoprenter.hu`; const expiresAt = new Date(Date.now() + (tokenData.expires_in * 1000)); // 1. Create or update stores record const { data: store, error: storeError } = await supabase .from('stores') .upsert({ user_id: userId, platform_name: 'shoprenter', store_name: shopname, store_url: `https://${shopDomain}`, shoprenter_app_id: config.appId, shoprenter_client_id: config.clientId, is_active: true, connected_at: new Date().toISOString() }, { onConflict: 'user_id,platform_name,store_url' }) .select() .single(); if (storeError) { console.error('[ShopRenter] Error storing store:', storeError); throw storeError; } // 2. Store OAuth tokens const { data: tokens, error: tokensError } = await supabase .from('shoprenter_tokens') .upsert({ store_id: store.id, access_token: tokenData.access_token, refresh_token: tokenData.refresh_token, token_type: tokenData.token_type, expires_at: expiresAt.toISOString(), scopes: tokenData.scope ? tokenData.scope.split(' ') : config.scopes, shopname: shopname, shop_domain: shopDomain, is_active: true }, { onConflict: 'store_id' }) .select() .single(); if (tokensError) { console.error('[ShopRenter] Error storing tokens:', tokensError); throw tokensError; } console.log(`[ShopRenter] Credentials stored for ${shopname}`); return { store, tokens }; } /** * Refresh access token * @param {string} shopname - Store name * @param {string} refreshToken - Refresh token * @returns {Promise} - New token data */ async function refreshAccessToken(shopname, refreshToken) { const tokenUrl = `https://${shopname}.myshoprenter.hu/oauth/token`; try { const response = await axios.post(tokenUrl, { grant_type: 'refresh_token', client_id: config.clientId, client_secret: config.clientSecret, refresh_token: refreshToken }); console.log(`[ShopRenter] Token refreshed for ${shopname}`); return response.data; } catch (error) { console.error('[ShopRenter] Token refresh error:', error.response?.data || error.message); throw new Error('Failed to refresh token'); } } /** * Get valid access token (refreshes if needed) * @param {string} storeId - Store ID * @returns {Promise} - Valid access token */ async function getValidAccessToken(storeId) { // Fetch tokens from database const { data: tokens, error } = await supabase .from('shoprenter_tokens') .select('*') .eq('store_id', storeId) .eq('is_active', true) .single(); if (error || !tokens) { throw new Error('ShopRenter tokens not found'); } // Check if token is expired or about to expire const expiresAt = new Date(tokens.expires_at); const now = new Date(); const bufferTime = config.tokenExpiryBuffer * 1000; // 5 minutes if (expiresAt.getTime() - now.getTime() < bufferTime) { // Token expired or expiring soon, refresh it console.log(`[ShopRenter] Token expiring, refreshing for ${tokens.shopname}`); const newTokenData = await refreshAccessToken(tokens.shopname, tokens.refresh_token); const newExpiresAt = new Date(Date.now() + (newTokenData.expires_in * 1000)); // Update database await supabase .from('shoprenter_tokens') .update({ access_token: newTokenData.access_token, refresh_token: newTokenData.refresh_token || tokens.refresh_token, expires_at: newExpiresAt.toISOString(), updated_at: new Date().toISOString() }) .eq('id', tokens.id); return newTokenData.access_token; } return tokens.access_token; } module.exports = { exchangeCodeForToken, storeCredentials, refreshAccessToken, getValidAccessToken }; ``` ### 4. API Routes (`api/index.js`) Add these routes to your main server file: ```javascript const { validateRequest } = require('../lib/shoprenter/hmac-validator'); const { exchangeCodeForToken, storeCredentials } = require('../lib/shoprenter/oauth'); // ShopRenter OAuth Callback app.get('/auth/shoprenter/callback', async (req, res) => { const { shopname, code, timestamp, hmac, app_url } = req.query; console.log(`[ShopRenter] OAuth callback received for ${shopname}`); // 1. Validate request const validation = validateRequest(req.query); if (!validation.valid) { console.error(`[ShopRenter] Validation failed: ${validation.error}`); return res.status(403).json({ error: 'Invalid request', details: validation.error }); } try { // 2. Exchange code for tokens const tokenData = await exchangeCodeForToken(shopname, code); // 3. Store credentials // Note: We need user_id here. In ShopRenter flow, we might need to: // - Store shopname in a temp Map with code // - Let user authenticate first // For now, we'll redirect to app_url and handle auth there // Store in temporary map for later association const installationId = crypto.randomBytes(16).toString('hex'); global.pendingShopRenterInstalls = global.pendingShopRenterInstalls || new Map(); global.pendingShopRenterInstalls.set(installationId, { shopname, tokenData, timestamp: Date.now(), expiresAt: Date.now() + (15 * 60 * 1000) // 15 minutes }); // 4. Redirect to app_url with installation_id const redirectUrl = new URL(app_url); redirectUrl.searchParams.append('sr_install', installationId); console.log(`[ShopRenter] Redirecting to: ${redirectUrl.toString()}`); res.redirect(redirectUrl.toString()); } catch (error) { console.error('[ShopRenter] OAuth callback error:', error); res.status(500).json({ error: 'Installation failed', details: error.message }); } }); // ShopRenter EntryPoint (after redirect from callback) app.get('/integrations/shoprenter/entry', securedSession, async (req, res) => { const { shopname, code, timestamp, hmac, sr_install } = req.query; console.log(`[ShopRenter] EntryPoint accessed for ${shopname}`); // 1. Validate HMAC const validation = validateRequest({ shopname, code, timestamp, hmac }); if (!validation.valid) { return res.status(403).json({ error: 'Invalid request', details: validation.error }); } try { // 2. Get installation data from temporary storage if (!sr_install || !global.pendingShopRenterInstalls?.has(sr_install)) { return res.status(400).json({ error: 'Installation session not found or expired' }); } const installData = global.pendingShopRenterInstalls.get(sr_install); // 3. Store credentials with authenticated user await storeCredentials(req.user.id, installData.shopname, installData.tokenData); // 4. Clean up temporary storage global.pendingShopRenterInstalls.delete(sr_install); // 5. Set up webhooks (async) setupWebhooks(req.user.id, installData.shopname).catch(err => { console.error('[ShopRenter] Webhook setup error:', err); }); // 6. Start initial sync (async) syncShopRenterData(req.user.id, installData.shopname).catch(err => { console.error('[ShopRenter] Initial sync error:', err); }); // 7. Redirect to success page res.redirect('https://shopcall.ai/integrations?connected=shoprenter&status=success'); } catch (error) { console.error('[ShopRenter] EntryPoint error:', error); res.redirect('https://shopcall.ai/integrations?connected=shoprenter&status=error'); } }); // ShopRenter Uninstall Webhook app.get('/webhooks/shoprenter/uninstall', async (req, res) => { const { shopname, code, timestamp, hmac } = req.query; console.log(`[ShopRenter] Uninstall webhook received for ${shopname}`); // 1. Validate HMAC const validation = validateRequest(req.query); if (!validation.valid) { return res.status(403).json({ error: 'Invalid request', details: validation.error }); } try { // 2. Find store by shopname const { data: tokens } = await supabase .from('shoprenter_tokens') .select('store_id') .eq('shopname', shopname) .single(); if (tokens) { // 3. Deactivate store await supabase .from('stores') .update({ is_active: false, updated_at: new Date().toISOString() }) .eq('id', tokens.store_id); // 4. Deactivate tokens await supabase .from('shoprenter_tokens') .update({ is_active: false, updated_at: new Date().toISOString() }) .eq('store_id', tokens.store_id); console.log(`[ShopRenter] Store ${shopname} uninstalled successfully`); } // 5. Respond with 200 (ShopRenter expects this) res.status(200).json({ message: 'Uninstall processed' }); } catch (error) { console.error('[ShopRenter] Uninstall error:', error); // Still respond with 200 to prevent retries res.status(200).json({ message: 'Uninstall processed with errors' }); } }); // Cleanup expired installations periodically setInterval(() => { if (!global.pendingShopRenterInstalls) return; const now = Date.now(); for (const [key, value] of global.pendingShopRenterInstalls.entries()) { if (now > value.expiresAt) { global.pendingShopRenterInstalls.delete(key); console.log(`[ShopRenter] Cleaned up expired installation: ${key}`); } } }, 5 * 60 * 1000); // Every 5 minutes ``` --- ## 🎨 Frontend Implementation ### Integration UI Component Create: `/shopcall.ai-main/src/components/ShopRenterConnect.tsx` ```typescript import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Loader2, CheckCircle, AlertCircle } from 'lucide-react'; export function ShopRenterConnect() { const [shopname, setShopname] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const handleConnect = async () => { if (!shopname.trim()) { setError('KΓ©rjΓΌk, adja meg a bolt nevΓ©t'); return; } setLoading(true); setError(''); try { // For ShopRenter, the store owner must initiate installation from ShopRenter admin // This would typically open the ShopRenter app store window.open( `https://${shopname}.myshoprenter.hu/admin/app/${SHOPRENTER_CLIENT_ID}`, '_blank' ); // Show instructions setError(''); } catch (err) { setError('Hiba tΓΆrtΓ©nt a kapcsolΓ³dΓ‘s sorΓ‘n'); console.error('ShopRenter connection error:', err); } finally { setLoading(false); } }; return (
ShopRenter ShopRenter

Magyar webΓ‘ruhΓ‘z platform - AI telefonos asszisztens integrΓ‘ciΓ³

setShopname(e.target.value)} className="bg-slate-700 border-slate-600 text-white flex-1" /> .myshoprenter.hu

Add meg a bolt nevΓ©t (pl. "pelda" ha a bolt cΓ­me pelda.myshoprenter.hu)

{error && ( {error} )}

TelepΓ­tΓ©si lΓ©pΓ©sek:

  1. Kattints a "Kapcsolat lΓ©trehozΓ‘sa" gombra
  2. Jelentkezz be a ShopRenter admin felΓΌletedre
  3. Hagyd jΓ³vΓ‘ az alkalmazΓ‘s telepΓ­tΓ©sΓ©t
  4. VisszairΓ‘nyΓ­tunk ide a sikeres telepΓ­tΓ©s utΓ‘n
); } ``` ### Add to Integrations Page Update `/shopcall.ai-main/src/components/IntegrationsContent.tsx`: ```typescript import { ShopRenterConnect } from './ShopRenterConnect'; // In the platforms grid, add:
{/* Existing Shopify card */} {/* Existing WooCommerce card */} {/* New ShopRenter card */}
``` --- ## πŸ”’ Security Considerations ### 1. HMAC Validation - βœ… **Always validate HMAC** on every request from ShopRenter - βœ… Use timing-safe comparison (`crypto.timingSafeEqual`) - βœ… Validate timestamp to prevent replay attacks (5-minute window) ### 2. Token Storage - βœ… Store access tokens encrypted in database - βœ… Never log or expose tokens in responses - βœ… Implement automatic token refresh - βœ… Revoke tokens on uninstall ### 3. API Communication - βœ… Always use HTTPS for all endpoints - βœ… Implement rate limiting (5 req/sec per ShopRenter guidelines) - βœ… Handle API errors gracefully - βœ… Implement retry logic with exponential backoff ### 4. Data Privacy (GDPR Compliance) - βœ… Only request necessary scopes - βœ… Implement data deletion on uninstall - βœ… Provide clear privacy policy - βœ… Handle customer data securely ### 5. Environment Variables ```bash # .env for backend SHOPRENTER_APP_ID=your_app_id_here SHOPRENTER_CLIENT_ID=your_client_id_here SHOPRENTER_CLIENT_SECRET=your_client_secret_here ``` **⚠️ Never commit these to version control!** --- ## πŸ§ͺ Testing Strategy ### 1. Test Store Setup 1. Request test store at: https://www.shoprenter.hu/tesztigenyles/?devstore=1 2. Store name: `shopcall-test-store` 3. URL: `shopcall-test-store.myshoprenter.hu` ### 2. Test Cases #### OAuth Flow Testing ``` Test Case 1: Successful Installation - Initiate OAuth from ShopRenter admin - Verify HMAC validation passes - Verify token exchange succeeds - Verify credentials stored in database - Verify redirect to success page Test Case 2: Invalid HMAC - Modify HMAC parameter - Verify request rejected with 403 - Verify error logged Test Case 3: Expired Timestamp - Use timestamp older than 5 minutes - Verify request rejected - Verify error logged Test Case 4: Uninstall Flow - Trigger uninstall from ShopRenter - Verify webhook received - Verify store deactivated - Verify tokens deactivated ``` #### API Testing ``` Test Case 5: Product Sync - Request product list from API - Verify products cached in database - Verify product data accuracy Test Case 6: Token Refresh - Wait for token expiry - Make API request - Verify token auto-refreshed - Verify new token stored Test Case 7: Webhook Processing - Create test order in ShopRenter - Verify webhook received - Verify order data processed - Verify response 200 sent ``` ### 3. Integration Testing Tools ```javascript // test/shoprenter-integration.test.js const axios = require('axios'); const crypto = require('crypto'); describe('ShopRenter Integration', () => { const testShop = 'shopcall-test-store'; const clientSecret = process.env.SHOPRENTER_CLIENT_SECRET; function generateValidHMAC(params) { const sorted = Object.keys(params) .sort() .map(key => `${key}=${params[key]}`) .join('&'); return crypto .createHmac('sha256', clientSecret) .update(sorted) .digest('hex'); } it('should validate correct HMAC', async () => { const params = { shopname: testShop, code: 'test123', timestamp: Math.floor(Date.now() / 1000).toString() }; params.hmac = generateValidHMAC(params); const response = await axios.get('http://localhost:3000/auth/shoprenter/callback', { params, maxRedirects: 0, validateStatus: () => true }); expect(response.status).not.toBe(403); }); it('should reject invalid HMAC', async () => { const params = { shopname: testShop, code: 'test123', timestamp: Math.floor(Date.now() / 1000).toString(), hmac: 'invalid_hmac' }; const response = await axios.get('http://localhost:3000/auth/shoprenter/callback', { params, maxRedirects: 0, validateStatus: () => true }); expect(response.status).toBe(403); }); }); ``` --- ## πŸš€ Deployment Plan ### Phase 1: Development (Week 1-2) - [ ] Set up test ShopRenter store - [ ] Implement HMAC validation - [ ] Implement OAuth flow - [ ] Create database tables - [ ] Build API client - [ ] Test with test store ### Phase 2: Integration (Week 3) - [ ] Implement product sync - [ ] Implement webhook handlers - [ ] Create frontend UI - [ ] Test end-to-end flow - [ ] Handle error scenarios ### Phase 3: Testing (Week 4) - [ ] Unit tests - [ ] Integration tests - [ ] Security audit - [ ] Performance testing - [ ] User acceptance testing ### Phase 4: Production Registration (Week 5) - [ ] Submit app to ShopRenter Partner Support - [ ] Provide all required data - [ ] Upload logo and assets - [ ] Wait for approval - [ ] Receive production credentials ### Phase 5: Launch (Week 6) - [ ] Deploy to production - [ ] Configure production environment variables - [ ] Monitor logs and errors - [ ] Soft launch with beta users - [ ] Public launch --- ## πŸ“… Timeline & Milestones | Week | Milestone | Deliverable | |------|-----------|-------------| | 1 | Setup & Planning | Test store, HMAC validator | | 2 | OAuth Implementation | Working OAuth flow | | 3 | API Integration | Product sync, webhooks | | 4 | Testing & QA | All tests passing | | 5 | Registration | App submitted to ShopRenter | | 6 | Production Launch | Live integration | **Total Estimated Time:** 6 weeks --- ## πŸ“š Resources & References ### Documentation - ShopRenter Developer Docs: https://doc.shoprenter.hu - API Reference: https://doc.shoprenter.hu/development/api/ - App Development Guide: https://doc.shoprenter.hu/development/app-development/ ### Example Apps - PHP Demo: https://github.com/Shoprenter/sr-demo-app-php - Node.js Demo: https://github.com/Shoprenter/sr-demo-app-node ### Contact - Partner Support: partnersupport@shoprenter.hu - Test Store Request: https://www.shoprenter.hu/tesztigenyles/?devstore=1 --- ## βœ… Checklist for Launch ### Pre-Registration - [ ] Review ShopRenter developer documentation - [ ] Prepare application name and description (Hungarian) - [ ] Design application logo (250x150px) - [ ] Set up HTTPS endpoints (EntryPoint, RedirectUri, UninstallUri) - [ ] Determine required API scopes - [ ] Request test store ### Development - [ ] Implement HMAC validation - [ ] Implement OAuth flow - [ ] Create database schema - [ ] Build API client - [ ] Implement product sync - [ ] Set up webhook handlers - [ ] Create frontend UI - [ ] Add error handling - [ ] Implement logging ### Testing - [ ] Test OAuth installation - [ ] Test HMAC validation - [ ] Test token refresh - [ ] Test product sync - [ ] Test webhooks - [ ] Test uninstall flow - [ ] Security testing - [ ] Performance testing ### Registration - [ ] Submit to partnersupport@shoprenter.hu - [ ] Provide all required information - [ ] Upload logo and assets - [ ] Wait for AppId, ClientId, ClientSecret - [ ] Configure environment variables ### Production - [ ] Deploy backend to production - [ ] Deploy frontend to production - [ ] Configure production URLs - [ ] Monitor logs for errors - [ ] Test with real store - [ ] Document usage for customers --- ## πŸ”„ Post-Launch Maintenance ### Regular Tasks - Monitor webhook reliability - Check token refresh logs - Review error rates - Update API client when ShopRenter updates - Sync product catalog daily - Respond to merchant support requests ### Scope Updates When new features require additional scopes: 1. Email Partner Support with new scope list 2. Wait for scope approval 3. Direct existing merchants to approve new scopes: `https://{shopName}.myshoprenter.hu/admin/app/{clientId}/approveScopes` 4. Update app to use new scopes --- **Document Version:** 1.0 **Last Updated:** 2025-10-22 **Author:** ShopCall.ai Development Team **Status:** Ready for Implementation