@getverdict/mock-bridge
v1.3.0
Published
Mock Shopify App Bridge for testing and development
Maintainers
Readme
Shopify Mock Bridge
A comprehensive browser testing solution for Shopify embedded apps. Mock the Shopify Admin environment and App Bridge APIs locally without needing real Shopify credentials, captchas, or 2FA.
🎥 See It In Action
▶️ Watch the Demo Video - See how Mock Bridge simplifies Shopify app testing
🎯 Why Use This Package?
Testing Shopify embedded apps is hard:
- 🚫 Shopify Admin requires 2FA and captchas
- 🤖 Playwright/automation tools can't bypass security
- 🔒 Chrome DevTools MCP can't interact with your embedded Shopify app
- 🌐 CI/CD pipelines need internet and credentials
- 🐛 Manual testing in real admin is slow and unreliable
This package solves all of that:
- ✅ No captchas, 2FA, or real Shopify account needed
- ✅ Full Playwright and automation support
- ✅ Chrome MCP and DevTools compatibility
- ✅ Works offline and in CI/CD pipelines
- ✅ Real database integration for comprehensive testing
🗺️ API Support
Core Features
| Feature | Status | Notes |
|---------|--------|-------|
| iFrame embed | ✅ Supported | Full admin frame with Polaris styling |
| Session tokens | ✅ Supported | shopify.idToken() |
| Mock environment detection | ✅ Supported | Auto-loads mock App Bridge |
App Bridge APIs
| API | Status | Notes |
|-----|--------|-------|
| shopify.modal | ✅ Supported | show(), hide(), toggle() |
| shopify.saveBar | ✅ Supported | show(), hide(), toggle() |
| shopify.loading | ✅ Supported | loading(boolean) |
| shopify.toast | ✅ Supported | show(message, options) |
| shopify.idToken | ✅ Supported | Returns valid JWT |
| shopify.config | ✅ Supported | apiKey, shop, locale |
| shopify.environment | ✅ Supported | embedded, mobile, pos |
| shopify.user | ✅ Supported | Returns mock user object |
| shopify.scopes | 🔶 Stub | Returns mock data |
| shopify.resourcePicker | 🔶 Stub | Returns empty array |
| shopify.picker | 🔶 Stub | Returns empty selection |
| shopify.scanner | 🔶 Stub | Returns mock scan data |
| shopify.pos | 🔶 Stub | Cart API with mock data |
| shopify.intents | 🔶 Stub | invoke(), register() |
| shopify.webVitals | 🔶 Stub | Callback registration only |
| shopify.support | 🔶 Stub | Callback registration only |
| shopify.reviews | 🔶 Stub | Returns success |
| shopify.app | 🔶 Stub | Returns empty extensions |
| Authenticated fetch | ✅ Supported | Mock, proxy, or direct modes |
| Navigation | ❌ Not implemented | |
| Print | ❌ Not implemented | |
| Share | ❌ Not implemented | |
Web Components
| Component | Status | Notes |
|-----------|--------|-------|
| <ui-modal> | ✅ Supported | With <ui-title-bar> support |
| <ui-save-bar> | ✅ Supported | With data-save-bar form integration |
| <ui-nav-menu> | ✅ Supported | Displays in admin sidebar |
| <ui-title-bar> | ✅ Supported | Inside modals |
| <s-app-window> | ❌ Not implemented | |
Legend: ✅ Supported | 🔶 Stub (returns mock data) | ❌ Not implemented
📦 Installation
npm install @getverdict/mock-bridge --save-dev
# or
yarn add @getverdict/mock-bridge --dev
# or
pnpm add @getverdict/mock-bridge --save-dev⚡ Quick Start (2 Commands)
# 1. Install the package
npm install @getverdict/mock-bridge --save-dev
# 2. Start the mock (just provide your app URL)
npx @getverdict/mock-bridge http://localhost:3000That's it! Your app is now running in a mock Shopify Admin at http://localhost:3080
🚀 Quick Start Guide
Step 1: Start the Mock Server
Option A: One-Command Start (Recommended)
# Simplest - just provide your app URL
npx @getverdict/mock-bridge http://localhost:3000
# Auto-detects client ID from SHOPIFY_API_KEY environment variable
# Auto-detects common app paths and configurationsOption B: Configuration File
# Generate a config file
npx @getverdict/mock-bridge init
# Edit the generated mock.config.js, then run:
npx @getverdict/mock-bridge# Add to package.json scripts for easy access
{
"scripts": {
"dev": "next dev",
"mock:admin": "mock-bridge http://localhost:3000",
"dev:mock": "concurrently \"npm run dev\" \"npm run mock:admin\""
}
}Option C: Programmatic API
If you need more control, you can still use the programmatic API:
// scripts/start-mock-admin.js
const { MockShopifyAdminServer } = require("@verdict/mock-bridge");
async function startMockAdmin() {
const server = new MockShopifyAdminServer({
appUrl: "http://localhost:3000",
clientId: process.env.SHOPIFY_API_KEY,
clientSecret: "mock-secret-12345",
port: 3080,
debug: true,
});
await server.start();
console.log("🎉 Mock Shopify Admin ready at http://localhost:3080");
}
startMockAdmin().catch(console.error);Step 2: Frontend Integration
Enable your app to detect and use the mock environment:
Option A: Automatic Detection (Recommended)
Replace your App Bridge script loading:
<!-- Before: Direct CDN loading -->
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
<!-- After: Smart loading with mock support -->
<script>
// Check if we're in a mock environment
const urlParams = new URLSearchParams(window.location.search);
const isEmbedded = urlParams.get("embedded") === "1";
const host = urlParams.get("host");
let isMockEnvironment = false;
// Detect mock environment from URL parameters
if (isEmbedded && host) {
try {
const decodedHost = atob(host);
if (
decodedHost.includes("localhost") ||
decodedHost.includes("mock") ||
window.location.hostname === "localhost"
) {
isMockEnvironment = true;
}
} catch (e) {
// Ignore decode errors
}
}
// Load appropriate App Bridge
if (isMockEnvironment) {
console.log("Loading Mock App Bridge");
const script = document.createElement("script");
script.src = "http://localhost:3080/app-bridge.js";
script.onerror = () => {
// Fallback to real CDN if mock fails
const fallback = document.createElement("script");
fallback.src = "https://cdn.shopify.com/shopifycloud/app-bridge.js";
document.head.appendChild(fallback);
};
document.head.appendChild(script);
} else {
console.log("Loading Real Shopify App Bridge");
const script = document.createElement("script");
script.src = "https://cdn.shopify.com/shopifycloud/app-bridge.js";
document.head.appendChild(script);
}
</script>Option B: Package Utility (TypeScript)
// app.tsx or _app.tsx
import { setupAppBridge } from "@getverdict/mock-bridge/client";
useEffect(() => {
setupAppBridge({
debug: true,
onMockDetected: (mockServerUrl) => {
console.log("Mock environment detected:", mockServerUrl);
},
onShopifyDetected: () => {
console.log("Real Shopify environment detected");
},
})
.then(() => {
console.log("App Bridge loaded successfully");
})
.catch(console.error);
}, []);Step 3: Backend Integration
Make your backend support mock session tokens alongside real ones:
Quick Integration (Replace existing JWT validation)
// Before: Only real Shopify tokens
import { verifyShopifyJWT } from "./your-auth";
export async function authenticate(token: string) {
const authData = await verifyShopifyJWT(token);
// ... rest of auth logic
}// After: Support both real and mock tokens
import {
validateSessionToken,
createMockUser,
} from "@getverdict/mock-bridge/auth";
export async function authenticate(token: string) {
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
});
if (!authData) {
throw new Error("Invalid token");
}
// Get shop from your database (same for both mock and real)
const shop = await getShopByName(authData.shopName);
if (!shop) {
throw new Error("Shop not found");
}
if (authData.isMock) {
// Mock environment - skip Shopify API calls
return {
shop,
currentUser: createMockUser({
shopName: authData.shopName,
permissions: shop.settings?.defaultStaffPermissions,
}),
isMock: true,
};
} else {
// Real environment - proceed with normal Shopify flow
const currentUser = await exchangeTokenForUser(token, shop);
return {
shop,
currentUser,
isMock: false,
};
}
}Framework-Specific Examples
Next.js API Routes:
// pages/api/products.ts
import { validateSessionToken } from "@getverdict/mock-bridge/auth";
export default async function handler(req, res) {
const token = req.headers.authorization?.replace("Bearer ", "");
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
});
if (!authData) {
return res.status(401).json({ error: "Unauthorized" });
}
if (authData.isMock) {
// Return mock data for testing
return res.json({
products: [
{ id: "1", title: "Mock Product 1", price: "19.99" },
{ id: "2", title: "Mock Product 2", price: "29.99" },
],
});
} else {
// Fetch real products from Shopify
const products = await fetchShopifyProducts(authData.shopName);
return res.json({ products });
}
}Express.js Middleware:
import {
validateSessionToken,
createMockUser,
} from "@getverdict/mock-bridge/auth";
function createAuthMiddleware() {
return async (req, res, next) => {
const token = req.headers.authorization?.replace("Bearer ", "");
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
});
if (!authData) {
return res.status(401).json({ error: "Unauthorized" });
}
const shop = await getShopByName(authData.shopName);
req.shop = shop;
req.isMockAuth = authData.isMock;
if (authData.isMock) {
req.currentUser = createMockUser({ shopName: authData.shopName });
} else {
req.currentUser = await exchangeTokenForUser(token, shop);
}
next();
};
}
app.use("/api/*", createAuthMiddleware());Step 4: Database Setup
Ensure your mock shop exists in the database:
// Add this to your database seed or setup script
async function setupMockShop() {
const mockShop = {
name: "test-shop.myshopify.com",
displayName: "Mock Test Shop",
accessToken: "mock-access-token",
active: true,
settings: {
defaultStaffPermissions: [
"read_products",
"write_products",
"read_orders",
"write_orders",
],
},
};
await createOrUpdateShop(mockShop);
console.log("Mock shop created for testing");
}
// Run during development setup
if (process.env.NODE_ENV === "development") {
setupMockShop();
}Step 5: Start Development
# Option 1: CLI command (simplest)
npx @getverdict/mock-bridge http://localhost:3000
# Option 2: Package.json scripts
npm run dev:mock
# Option 3: Separate terminals
npm run dev # Terminal 1: Your app
npm run mock:admin # Terminal 2: Mock adminThen navigate to:
- Your app: http://localhost:3000
- Mock Shopify Admin: http://localhost:3080
- Your app embedded in mock admin: http://localhost:3080 (automatically embeds your app)
🖥️ CLI Reference
Quick Commands
# Basic usage with auto-detection
npx @getverdict/mock-bridge http://localhost:3000
# If installed locally, you can use the shorter command:
# npm install @getverdict/mock-bridge --save-dev
# npx mock-bridge http://localhost:3000
# Full configuration
npx @getverdict/mock-bridge http://localhost:3000/shopify \
--client-id your-client-id \
--port 3080 \
--debug
# Using config file
npx @getverdict/mock-bridge init # Create config file
npx @getverdict/mock-bridge # Use config file
# Help and version
npx @getverdict/mock-bridge --help
npx @getverdict/mock-bridge --versionCLI Options
| Option | Description | Default |
| ----------------- | ------------------------------------ | ------------------------------- |
| [app-url] | Your app's URL (positional argument) | Auto-detected from package.json |
| --client-id | Shopify app client ID | $SHOPIFY_API_KEY |
| --client-secret | Mock client secret | "mock-secret-12345" |
| --shop | Mock shop domain | "test-shop.myshopify.com" |
| --port | Mock admin port | 3080 |
| --config | Config file path | "mock.config.js" |
| --debug | Enable debug logging | false |
Environment Variables
The CLI automatically reads these environment variables:
SHOPIFY_API_KEY=your-client-id # Used for --client-id
NODE_ENV=development # Enables mock token supportConfiguration File
Generate a configuration file with npx @getverdict/mock-bridge init:
// mock.config.js
module.exports = {
appUrl: "http://localhost:3000/shopify", // Include path in URL
clientId: process.env.SHOPIFY_API_KEY,
clientSecret: "mock-secret-12345",
port: 3080,
shop: "test-shop.myshopify.com",
debug: true,
scopes: ["read_products", "write_products", "read_orders", "write_orders"],
};🎭 How It Works
Architecture Overview
┌─────────────────────────────────────┐
│ Mock Shopify Admin (Port 3080) │
│ ┌─────────────────────────────┐ │
│ │ Shopify Admin UI │ │
│ │ (Navigation, etc.) │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Your App (iframe) │ │ ← Embedded like real Shopify
│ │ - Mock App Bridge loaded │ │
│ │ - Gets mock session tokens │ │
│ │ - Makes API calls to your │ │
│ │ backend │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
↕ PostMessage API
┌─────────────────────────────────────┐
│ Your Backend │
│ - Validates mock tokens │ ← Same backend, enhanced auth
│ - Skips Shopify API calls │
│ - Uses real database │
│ - Returns mock/real data │
└─────────────────────────────────────┘Mock vs Real Flow
Mock Environment (Testing):
- Mock admin serves your app in iframe
- Mock App Bridge provides session tokens
- Your frontend makes API calls to your backend
- Backend detects mock tokens and skips Shopify APIs
- Returns mock data or uses database directly
Real Environment (Production):
- Real Shopify admin serves your app in iframe
- Real App Bridge provides session tokens
- Your frontend makes API calls to your backend
- Backend detects real tokens and calls Shopify APIs
- Returns real data from Shopify
🔧 Configuration Options
Mock Server Configuration
const server = new MockShopifyAdminServer({
// Required
appUrl: "http://localhost:3000/shopify", // Your app's URL (with path)
clientId: "your-shopify-client-id", // Your Shopify app's client ID
clientSecret: "mock-secret-12345", // Mock secret (dev only)
// Optional
port: 3080, // Mock admin port
shop: "test-shop.myshopify.com", // Mock shop domain
apiVersion: "2024-01", // Shopify API version
scopes: [
// Your app's scopes
"read_products",
"write_products",
"read_orders",
"write_orders",
],
debug: true, // Enable debug logging
// Admin API handling (see below)
adminApi: "mock",
});Admin API Configuration
Control how fetch('/admin/api/...') requests are handled:
// mock.config.js
// Option 1: Mock data (default) - returns fake data, works offline
module.exports = {
adminApi: "mock",
};
// Option 2: Proxy through your app - for real data via your backend
module.exports = {
adminApi: {
proxy: "http://localhost:3000/api/shopify-proxy",
},
};
// Option 3: Direct to Shopify - requires access token from installed shop
module.exports = {
adminApi: {
accessToken: process.env.SHOPIFY_ACCESS_TOKEN,
},
};When to use each mode:
| Mode | Use Case |
|------|----------|
| 'mock' | Offline testing, CI/CD, no Shopify credentials needed |
| { proxy: '...' } | Real data testing via your app's backend proxy |
| { accessToken: '...' } | Direct Shopify API access (requires installed shop) |
Proxy endpoint example (if using proxy mode):
// pages/api/shopify-proxy.ts (Next.js example)
export default async function handler(req, res) {
const { url, method, body } = req.body;
const shop = await getShopFromSession(req);
const response = await fetch(`https://${shop.domain}${url}`, {
method,
headers: {
"X-Shopify-Access-Token": shop.accessToken,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
res.json(await response.json());
}Authentication Options
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!, // Required
mockSecret: "custom-mock-secret", // Optional
developmentOnly: true, // Only try mock in dev
});Mock User Options
const mockUser = createMockUser({
shopName: "test-shop.myshopify.com",
userId: "123456789", // Custom user ID
email: "[email protected]", // Custom email
firstName: "Test", // Custom first name
lastName: "User", // Custom last name
permissions: ["read_products"], // Custom permissions
additionalProps: {
// Any additional props
posBillingTermsAcceptedAt: new Date(),
customField: "value",
},
});🎪 Testing with Automation Tools
Playwright Example
// playwright.config.ts
import { defineConfig } from "@playwright/test";
import { MockShopifyAdminServer } from "@getverdict/mock-bridge";
let mockServer: MockShopifyAdminServer;
export default defineConfig({
globalSetup: async () => {
mockServer = new MockShopifyAdminServer({
appUrl: "http://localhost:3000",
clientId: "test-client-id",
clientSecret: "mock-secret-12345",
port: 3080,
});
await mockServer.start();
},
globalTeardown: async () => {
await mockServer?.stop();
},
use: {
baseURL: "http://localhost:3080",
},
});// tests/app.spec.ts
import { test, expect } from "@playwright/test";
test("should load app in mock Shopify admin", async ({ page }) => {
await page.goto("/");
// Wait for app to load in iframe
const appFrame = page.frameLocator("#app-iframe");
// Test your app functionality
await expect(appFrame.locator("h1")).toContainText("Your App Title");
// Test App Bridge actions
await appFrame.locator('button:has-text("Show Toast")').click();
await expect(page.locator(".toast")).toContainText("Success!");
// Test API calls
await appFrame.locator('button:has-text("Load Products")').click();
await expect(appFrame.locator(".product-list")).toBeVisible();
});
test("should handle authentication", async ({ page }) => {
await page.goto("/");
const appFrame = page.frameLocator("#app-iframe");
// Your app should be authenticated automatically
await expect(appFrame.locator(".user-info")).toContainText("Mock User");
await expect(appFrame.locator(".shop-info")).toContainText(
"test-shop.myshopify.com"
);
});Jest Integration Testing
// tests/api.test.ts
import { MockShopifyAdminServer } from "@getverdict/mock-bridge";
import {
createMockUser,
validateSessionToken,
} from "@verdict/shopify-app-bridge-mock/auth";
describe("API with Mock Tokens", () => {
let mockServer: MockShopifyAdminServer;
beforeAll(async () => {
mockServer = new MockShopifyAdminServer({
appUrl: "http://localhost:3000",
clientId: "test-client",
clientSecret: "mock-secret-12345",
});
await mockServer.start();
});
afterAll(async () => {
await mockServer.stop();
});
it("should authenticate with mock token", async () => {
// Generate mock token
const token = mockServer.tokenGenerator.generateSessionToken({
shop: "test-shop.myshopify.com",
clientId: "test-client",
clientSecret: "mock-secret-12345",
});
// Test authentication
const authData = await validateSessionToken(token, {
shopifySecret: "real-secret",
mockSecret: "mock-secret-12345",
});
expect(authData?.isMock).toBe(true);
expect(authData?.shopName).toBe("test-shop.myshopify.com");
});
it("should create mock users", async () => {
const mockUser = createMockUser({
shopName: "test-shop.myshopify.com",
permissions: ["read_products"],
});
expect(mockUser.email).toBe("[email protected]");
expect(mockUser.permissions).toContain("read_products");
});
});🛡️ Security Considerations
Development Only
Mock tokens are designed for development and testing only:
// ✅ Good: Mock tokens only in development
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
developmentOnly: true, // Default: true
});
// ❌ Bad: Never allow mock tokens in production
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
developmentOnly: false, // Don't do this!
});Environment Variables
Keep mock secrets out of production:
# .env.local (development only)
SHOPIFY_API_PRIVATE_KEY=your-real-shopify-secret
MOCK_SECRET=mock-secret-12345
# .env.production (no mock secrets)
SHOPIFY_API_PRIVATE_KEY=your-real-shopify-secret
# No MOCK_SECRET in production!Database Requirements
Mock tokens still require valid shops in your database:
// Always verify shop exists, even for mock tokens
const authData = await validateSessionToken(token, options);
if (authData) {
const shop = await getShopByName(authData.shopName);
if (!shop) {
// Reject both mock and real tokens if shop doesn't exist
throw new Error("Shop not found");
}
}🐛 Troubleshooting
Common Issues
App not loading in mock admin:
- Check CSP headers allow iframe embedding
- Verify your app URL is correct in mock config
- Ensure your app accepts
embedded=1parameter
Mock tokens not working:
- Verify
NODE_ENV=development - Check mock secret matches between frontend and backend
- Ensure shop exists in your database
Real tokens broken:
- Verify
SHOPIFY_API_PRIVATE_KEYis set correctly - Check that real secret is passed to
validateSessionToken - Ensure your existing Shopify auth flow is preserved
Debug Mode
Enable debug logging to troubleshoot:
// Mock server with debug
const server = new MockShopifyAdminServer({
// ... config
debug: true,
});
// Frontend with debug
setupAppBridge({
debug: true,
onMockDetected: (url) => console.log("Mock detected:", url),
onShopifyDetected: () => console.log("Real Shopify detected"),
});Environment Check
Verify your environment setup:
// Add to your app startup
console.log("Environment:", process.env.NODE_ENV);
console.log("Has Shopify Secret:", !!process.env.SHOPIFY_API_PRIVATE_KEY);
console.log("Mock tokens enabled:", process.env.NODE_ENV === "development");📚 API Reference
Authentication Functions
validateSessionToken(token, options)
Universal validator for both real and mock tokens.
Parameters:
token: string- JWT session tokenoptions.shopifySecret: string- Real Shopify client secretoptions.mockSecret?: string- Mock secret (default: "mock-secret-12345")options.developmentOnly?: boolean- Only try mock tokens in development (default: true)
Returns: AuthResult | false
isMockToken(token, mockSecret?)
Quick check if token is a mock token.
Returns: boolean
createMockUser(options?)
Generate mock user objects for testing.
Returns: MockCurrentUser
withMockTokenSupport(authFunction, shopifySecret, options?)
Wrapper to add mock support to existing auth functions.
Server Classes
MockShopifyAdminServer
Main mock server class.
Methods:
start()- Start the mock serverstop()- Stop the mock servergetConfig()- Get server configuration
Client Utilities
setupAppBridge(options?)
Automatically detect mock environment and load appropriate App Bridge.
Returns: Promise<void>
For complete API documentation, see Backend Integration Guide.
🤝 Contributing
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
Development Setup
git clone <repository>
cd packages/shopify-app-bridge-mock
pnpm install
pnpm build
pnpm test📄 License
MIT License - see LICENSE file for details.
🙏 Acknowledgments
- Built for the Shopify developer community
- Inspired by the need for better testing tools
- Thanks to all contributors and users
Made with ❤️ for Shopify developers who want to test without pain
