SHOPRENTER.md 41 KB

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
  2. Registration Requirements
  3. Technical Architecture
  4. OAuth Flow Implementation
  5. API Integration
  6. Database Schema
  7. Backend Implementation
  8. Frontend Implementation
  9. Security Considerations
  10. Testing Strategy
  11. Deployment Plan
  12. 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

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

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&timestamp=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&timestamp=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&timestamp=1337178173&hmac=d48e5d...&app_url=...

Validation Algorithm:

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&timestamp=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:

{
  "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:

{
  "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

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

-- 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

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

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)

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)

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)

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<Object>} - 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<Object>} - 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<Object>} - 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<string>} - 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:

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

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 (
    <Card className="bg-slate-800 border-slate-700">
      <CardHeader>
        <div className="flex items-center gap-3">
          <img
            src="/images/shoprenter-logo.png"
            alt="ShopRenter"
            className="w-10 h-10"
          />
          <CardTitle className="text-white">ShopRenter</CardTitle>
        </div>
        <p className="text-slate-400">
          Magyar webáruház platform - AI telefonos asszisztens integráció
        </p>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="space-y-2">
          <Label htmlFor="shopname" className="text-slate-300">
            Bolt neve
          </Label>
          <div className="flex gap-2">
            <Input
              id="shopname"
              placeholder="pelda"
              value={shopname}
              onChange={(e) => setShopname(e.target.value)}
              className="bg-slate-700 border-slate-600 text-white flex-1"
            />
            <span className="flex items-center text-slate-400">
              .myshoprenter.hu
            </span>
          </div>
          <p className="text-slate-500 text-sm">
            Add meg a bolt nevét (pl. "pelda" ha a bolt címe pelda.myshoprenter.hu)
          </p>
        </div>

        {error && (
          <Alert variant="destructive" className="bg-red-900/20 border-red-900">
            <AlertCircle className="w-4 h-4" />
            <AlertDescription>{error}</AlertDescription>
          </Alert>
        )}

        <Button
          onClick={handleConnect}
          disabled={loading}
          className="w-full bg-[#85b218] hover:bg-[#6d9315] text-white"
        >
          {loading ? (
            <>
              <Loader2 className="w-4 h-4 mr-2 animate-spin" />
              Kapcsolódás...
            </>
          ) : (
            'ShopRenter kapcsolat létrehozása'
          )}
        </Button>

        <div className="bg-slate-700/50 p-4 rounded-lg">
          <h4 className="text-white font-medium mb-2">Telepítési lépések:</h4>
          <ol className="text-slate-300 text-sm space-y-1 list-decimal list-inside">
            <li>Kattints a "Kapcsolat létrehozása" gombra</li>
            <li>Jelentkezz be a ShopRenter admin felületedre</li>
            <li>Hagyd jóvá az alkalmazás telepítését</li>
            <li>Visszairányítunk ide a sikeres telepítés után</li>
          </ol>
        </div>
      </CardContent>
    </Card>
  );
}

Add to Integrations Page

Update /shopcall.ai-main/src/components/IntegrationsContent.tsx:

import { ShopRenterConnect } from './ShopRenterConnect';

// In the platforms grid, add:
<div className="grid gap-6 md:grid-cols-3">
  {/* Existing Shopify card */}
  {/* Existing WooCommerce card */}

  {/* New ShopRenter card */}
  <ShopRenterConnect />
</div>

🔒 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

# .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

// 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

Example Apps

Contact


✅ 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