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/
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.
Must be sent to: partnersupport@shoprenter.hu
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)
⚠️ Note: The URLs below are examples. These are now configurable via environment variables:
FRONTEND_URL - for EntryPointBACKEND_URL - for RedirectUri and UninstallUriSee .env.example files for configuration details.
EntryPoint (HTTPS required):
https://shopcall.ai/integrations
shopname, code, timestamp, hmac, app_urlapp_url (provided by ShopRenter) with sr_install parameterRedirectUri (HTTPS required):
https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback
UninstallUri (HTTPS required):
https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/webhook-shoprenter-uninstall
Application Logo:
/shopcall.ai-main/public/images/shoprenter-app-logo.pngTest Store Name:
shopcall-test-store
shopcall-test-store.shoprenter.huBased 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:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ ShopRenter │ │ ShopCall.ai │ │ Supabase │
│ Store │◄───────►│ Backend │◄───────►│ Database │
│ │ OAuth │ │ Store │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ Webhooks │ API Calls
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Product Sync │ │ AI Phone │
│ Order Updates │ │ Assistant │
└─────────────────┘ └──────────────────┘
Frontend (React/Vite):
Backend (Supabase Edge Functions - Deno/TypeScript):
Database (Supabase):
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
ShopRenter sends:
GET /auth/shoprenter/callback?shopname=example&code=0907a61c0c8d55e99db179b68161bc00×tamp=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×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:
After successful HMAC validation, request access token:
Endpoint: (To be confirmed with ShopRenter Partner Support)
POST https://{shopname}.shoprenter.hu/oauth/token
Request Body:
{
"grant_type": "authorization_code",
"client_id": "{ClientId}",
"client_secret": "{ClientSecret}",
"code": "{code_from_callback}",
"redirect_uri": "https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback"
}
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200...",
"scope": "product:read customer:read order:read"
}
https://{shopname}.shoprenter.hu/api
Based on ShopRenter API documentation:
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
}
}
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"
}
]
}
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": [...]
}
]
}
Register Webhook:
POST /api/webhooks
Authorization: Bearer {access_token}
Content-Type: application/json
Body:
{
"event": "order/create",
"address": "https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/webhook-shoprenter-orders",
"active": true
}
Response:
{
"id": "d2ViaG9vay13ZWJob29rX2lkPTE=",
"event": "order/create",
"address": "https://...",
"active": true
}
Available Webhook Events:
order/create - New order createdorder/update - Order status changedproduct/create - New product addedproduct/update - Product information changedproduct/delete - Product deletedcustomer/create - New customer registeredcustomer/update - Customer information updatedshoprenter_tokensCREATE 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.shoprenter.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();
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);
shoprenter_products_cacheCREATE 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);
shoprenter_webhooksCREATE 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)
);
Current Implementation: Supabase Edge Functions (TypeScript/Deno)
supabase/functions/
├── oauth-shoprenter-init/
│ └── index.ts # OAuth flow initialization
├── oauth-shoprenter-callback/
│ └── index.ts # OAuth callback handler
├── webhook-shoprenter-uninstall/
│ └── index.ts # Uninstall webhook handler
├── shoprenter-products/
│ └── index.ts # Product sync endpoint
├── shoprenter-orders/
│ └── index.ts # Order sync endpoint
├── shoprenter-customers/
│ └── index.ts # Customer sync endpoint
├── shoprenter-sync/
│ └── index.ts # Manual sync trigger
├── shoprenter-scheduled-sync/
│ └── index.ts # Automated background sync (pg_cron)
└── _shared/
└── shoprenter-client.ts # ShopRenter API client library
Note: All Edge Functions are deployed as serverless functions on Supabase infrastructure.
Environment Variables (Supabase Edge Functions):
// Accessed via Deno.env.get() in Edge Functions
const config = {
clientId: Deno.env.get('SHOPRENTER_CLIENT_ID'),
clientSecret: Deno.env.get('SHOPRENTER_CLIENT_SECRET'),
// Supabase URLs
supabaseUrl: Deno.env.get('SUPABASE_URL'),
supabaseAnonKey: Deno.env.get('SUPABASE_ANON_KEY'),
supabaseServiceKey: Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'),
// Frontend URL
frontendUrl: Deno.env.get('FRONTEND_URL'),
// Production endpoints
redirectUri: 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback',
entryPoint: 'https://shopcall.ai/integrations',
uninstallUri: 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/webhook-shoprenter-uninstall',
// Required scopes
scopes: [
'product:read',
'customer:read',
'order:read',
'order:write',
'webhook:write'
],
// Rate limiting
maxRequestsPerSecond: 5,
tokenExpiryBuffer: 300, // Refresh 5 min before expiry
};
Configuration in supabase/.env:
SHOPRENTER_CLIENT_ID=your_client_id_here
SHOPRENTER_CLIENT_SECRET=your_client_secret_here
SUPABASE_URL=https://ztklqodcdjeqpsvhlpud.supabase.co
SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
FRONTEND_URL=https://shopcall.ai
INTERNAL_SYNC_SECRET=your_random_secure_secret
_shared/shoprenter-client.ts)Note: HMAC validation is implemented in the shared ShopRenter client library used by all Edge Functions.
import { createHmac, timingSafeEqual } from "https://deno.land/std@0.177.0/node/crypto.ts";
/**
* Validate HMAC signature from ShopRenter
* @param query - Query parameters from request
* @param clientSecret - ShopRenter client secret
* @returns True if HMAC is valid
*/
function validateHMAC(query: Record<string, string>, clientSecret: string): boolean {
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 = createHmac('sha256', clientSecret)
.update(sortedParams)
.digest('hex');
// Timing-safe comparison
try {
return timingSafeEqual(
new TextEncoder().encode(calculatedHmac),
new TextEncoder().encode(hmac)
);
} catch (error) {
console.error('[ShopRenter] HMAC comparison error:', error);
return false;
}
}
/**
* Validate request timestamp (prevent replay attacks)
* @param timestamp - Unix timestamp from request
* @param maxAgeSeconds - Maximum age allowed (default: 300 = 5 min)
* @returns True if timestamp is within valid range
*/
function validateTimestamp(timestamp: string, maxAgeSeconds = 300): boolean {
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 query - Query parameters
* @param clientSecret - ShopRenter client secret
* @returns Validation result
*/
function validateRequest(
query: Record<string, string>,
clientSecret: string
): { valid: boolean; error?: string } {
// 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, clientSecret)) {
return {
valid: false,
error: 'HMAC validation failed'
};
}
return { valid: true };
}
export {
validateHMAC,
validateTimestamp,
validateRequest
};
Implementation: The OAuth flow is handled by two Edge Functions:
oauth-shoprenter-init - Initiates OAuth flowoauth-shoprenter-callback - Handles OAuth callback and token exchangeExample TypeScript Code (simplified from actual implementation):
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
/**
* Exchange authorization code for access token
* @param shopname - Store name
* @param code - Authorization code
* @returns Token response
*/
async function exchangeCodeForToken(shopname: string, code: string) {
const tokenUrl = `https://${shopname}.shoprenter.hu/oauth/token`;
const clientId = Deno.env.get('SHOPRENTER_CLIENT_ID')!;
const clientSecret = Deno.env.get('SHOPRENTER_CLIENT_SECRET')!;
const redirectUri = 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/oauth-shoprenter-callback';
try {
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: code,
redirect_uri: redirectUri
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.statusText}`);
}
const data = await response.json();
console.log(`[ShopRenter] Token acquired for ${shopname}`);
return data;
} catch (error) {
console.error('[ShopRenter] Token exchange error:', error);
throw new Error('Failed to exchange code for token');
}
}
/**
* Store ShopRenter credentials in database
* Note: Actual implementation in oauth-shoprenter-callback Edge Function
*/
async function storeCredentials(userId: string, shopname: string, tokenData: any) {
const shopDomain = `${shopname}.shoprenter.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}`,
api_key: tokenData.access_token,
api_secret: tokenData.refresh_token,
token_expires_at: expiresAt.toISOString(),
scopes: tokenData.scope ? tokenData.scope.split(' ') : [],
is_active: true,
connected_at: new Date().toISOString()
})
.select()
.single();
if (storeError) {
console.error('[ShopRenter] Error storing store:', storeError);
throw storeError;
}
console.log(`[ShopRenter] Credentials stored for ${shopname}`);
return store;
}
// Additional functions for token refresh are implemented in the ShopRenter client library
// See: supabase/functions/_shared/shoprenter-client.ts
Note: The complete OAuth implementation including token refresh, HMAC validation, and credential storage is available in the deployed Edge Functions. The code examples above are simplified for documentation purposes.
All ShopRenter integration functionality is implemented as Supabase Edge Functions (TypeScript/Deno). Below is a summary of each function:
/functions/v1/oauth-shoprenter-init/functions/v1/oauth-shoprenter-callbackshopname, code, timestamp, hmac, app_urlpending_shoprenter_installs table/functions/v1/webhook-shoprenter-uninstallshopname, code, timestamp, hmac/functions/v1/shoprenter-products{ store_id: string }/functions/v1/shoprenter-orders{ store_id: string }/functions/v1/shoprenter-customers{ store_id: string }/functions/v1/shoprenter-sync{ store_id: string }/functions/v1/shoprenter-scheduled-syncAuthorization: Bearer {INTERNAL_SYNC_SECRET}sync_logs tableThe following Express.js/Node.js routes are NO LONGER USED. They have been replaced by the Supabase Edge Functions listed above.
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}.shoprenter.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">
.shoprenter.hu
</span>
</div>
<p className="text-slate-500 text-sm">
Add meg a bolt nevét (pl. "pelda" ha a bolt címe pelda.shoprenter.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>
);
}
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>
crypto.timingSafeEqual)# .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!
shopcall-test-storeshopcall-test-store.myshoprenter.huTest 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
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
// 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);
});
});
oauth-shoprenter-init - OAuth flow initializationoauth-shoprenter-callback - OAuth callback with HMAC validationwebhook-shoprenter-uninstall - Uninstall webhook handlershoprenter-products - Product data syncshoprenter-orders - Order data syncshoprenter-customers - Customer data syncshoprenter-sync - Manual full sync triggershoprenter-scheduled-sync - Automated hourly background sync (pg_cron)stores table updated for ShopRenterpending_shoprenter_installs table createdoauth_states table for OAuth flow state managementshoprenter_products_cache table for product cachingshoprenter_tokens table for token managementshoprenter_webhooks table for webhook trackingstore_sync_config table for per-store sync settingssync_logs table for scheduled sync monitoringShopRenterConnect.tsx component implementedSHOPRENTER_REGISTRATION.md created (submission-ready)SHOPRENTER.md updated with actual implementation.shoprenter.hu)/shopcall.ai-main/public/images/shoprenter-app-logo.png)| Category | Completed | Pending | Total | Progress |
|---|---|---|---|---|
| Backend Edge Functions | 8 | 0 | 8 | 100% ✅ |
| Database Tables | 8 | 0 | 8 | 100% ✅ |
| Security Features | 5 | 0 | 5 | 100% ✅ |
| Sync Features | 6 | 0 | 6 | 100% ✅ |
| Frontend Components | 1 | 0 | 1 | 100% ✅ |
| Documentation | 2 | 0 | 2 | 100% ✅ |
| Registration Items | 0 | 5 | 5 | 0% ⏳ |
Overall Technical Implementation: ✅ 100% Complete Overall Project Status: ⏳ 83% Complete (awaiting registration approval)
| Phase | Milestone | Status | Date |
|---|---|---|---|
| 1 | Setup & Planning | ✅ Complete | Oct 2024 |
| 2 | OAuth Implementation | ✅ Complete | Oct 2024 |
| 3 | API Integration | ✅ Complete | Oct 2024 |
| 4 | Scheduled Sync | ✅ Complete | Oct 2024 |
| 5 | Testing & QA | ✅ Complete | Oct 2024 |
| 6 | Documentation | ✅ Complete | Oct 31, 2025 |
| 7 | Registration | ⏳ Pending | Nov 2025 (estimated) |
| 8 | Production Launch | 📋 Ready | TBD (after approval) |
Status: Ready for ShopRenter Partner Support Submission
When new features require additional scopes:
https://{shopName}.shoprenter.hu/admin/app/{clientId}/approveScopesThe ShopRenter integration includes automated background synchronization using PostgreSQL's pg_cron extension.
How it Works:
trigger_shoprenter_scheduled_sync()) makes HTTP request to Edge Functionshoprenter-scheduled-sync Edge Function processes all active ShopRenter storessync_logs tableConfiguration:
:00)store_sync_config table15min, 30min, hourly, 6hours, dailyMonitoring:
-- View recent sync logs
SELECT * FROM sync_logs
WHERE platform = 'shoprenter'
ORDER BY created_at DESC
LIMIT 10;
-- Check store sync configuration
SELECT * FROM store_sync_config
WHERE store_id IN (
SELECT id FROM stores WHERE platform_name = 'shoprenter'
);
Manual Control:
-- Enable/disable sync for a store
SELECT set_store_sync_enabled('store-uuid', true);
-- Change sync frequency
SELECT set_store_sync_frequency('store-uuid', 'hourly');
Database Configuration Required:
-- In Supabase Dashboard → Project Settings → Database → Custom Postgres Configuration
app.internal_sync_secret = 'same_as_INTERNAL_SYNC_SECRET_in_env'
app.supabase_url = 'https://ztklqodcdjeqpsvhlpud.supabase.co'
Document Version: 2.0 Last Updated: 2025-10-31 Author: ShopCall.ai Development Team Status: ✅ Implementation Complete - Ready for Registration