This document provides specific guidelines for developing the Cosmo Shopify Sales Channel App, including best practices, verification requirements, and implementation patterns.
🎯 Overview
The Cosmo Shopify app is a Sales Channel App that allows merchants to:
- Connect their Cosmo account
- Publish products to the Cosmo marketplace
- Manage orders and analytics
- Provide AI-powered customer assistance via storefront widget
📋 Verification Requirements
Technical Performance (2025 Core Web Vitals)
- LCP (Largest Contentful Paint): ≤ 2.5 seconds
- CLS (Cumulative Layout Shift): ≤ 0.1
- INP (Interaction to Next Paint): ≤ 200 milliseconds
Embedded App Requirements
- ✅ App Bridge v3+ integration on every page
- ✅ Session token authentication
- ✅ Embedded admin experience (no external redirects)
- ✅ Mobile-responsive design
Sales Channel Specific
- ✅ Account connection flow with approval states
- ✅ Product publishing via ProductListing API
- ✅ Cart permalink generation for checkout
- ✅ Billing API integration
- ✅ Order creation and management
🏗️ Architecture Patterns
App Structure
shopify_app/
├── src/
│ ├── app/ # Next.js 15 app directory
│ │ ├── api/ # API routes
│ │ │ ├── auth/ # OAuth endpoints
│ │ │ ├── products/ # Product management
│ │ │ └── webhooks/ # Webhook handlers
│ │ ├── components/ # Page-specific components
│ │ ├── providers.tsx # App Bridge + Polaris setup
│ │ └── page.tsx # Main dashboard
│ ├── components/ # Reusable components
│ └── utils/ # Utility functions
├── extensions/
│ └── cosmo-widget/ # Theme app extension
└── shopify.app.toml # App configuration
Component Organization
// Page components in src/app/
// Reusable components in src/components/
// Utilities in src/utils/
// Example component structure
src/
├── app/
│ ├── products/
│ │ └── page.tsx # Products page
│ └── orders/
│ └── page.tsx # Orders page
├── components/
│ ├── AccountConnection.tsx # Account connection UI
│ ├── ProductPublishing.tsx # Product management
│ └── OrderManagement.tsx # Order handling
└── utils/
├── cartPermalink.ts # Cart utilities
└── sessionToken.ts # Auth utilities
🔧 Implementation Patterns
App Bridge Setup
// src/app/providers.tsx
"use client";
import "@shopify/polaris/build/esm/styles.css";
import { Provider as AppBridgeProvider } from "@shopify/app-bridge-react";
import { AppProvider as PolarisProvider } from "@shopify/polaris";
import en from "@shopify/polaris/locales/en.json";
export function Providers({ children }: { children: React.ReactNode }) {
const host =
typeof window !== "undefined"
? new URLSearchParams(window.location.search).get("host") ?? ""
: "";
const config = {
apiKey: process.env.NEXT_PUBLIC_SHOPIFY_API_KEY!,
host,
forceRedirect: true,
};
return (
<AppBridgeProvider config={config}>
<PolarisProvider i18n={en}>{children}</PolarisProvider>
</AppBridgeProvider>
);
}
Navigation Integration
// src/app/page.tsx
import { NavMenu } from "@shopify/app-bridge-react";
const navigationLinks = [
{ label: "Dashboard", destination: "/" },
{ label: "Products", destination: "/products" },
{ label: "Orders", destination: "/orders" },
{ label: "Analytics", destination: "/analytics" },
{ label: "Settings", destination: "/settings" },
];
export default function HomePage() {
return (
<>
<NavMenu navigationLinks={navigationLinks} />
{/* Page content */}
</>
);
}
Session Token Authentication
// src/utils/sessionToken.ts
import createApp from "@shopify/app-bridge";
import { getSessionToken } from "@shopify/app-bridge/utilities";
export async function getShopifySessionToken(
apiKey: string,
host: string,
): Promise<string> {
try {
const app = createApp({ apiKey, host });
const token = await getSessionToken(app);
return token;
} catch (error) {
console.error("Error getting session token:", error);
throw new Error("Failed to get session token");
}
}
export async function authenticatedFetch(
url: string,
options: RequestInit = {},
apiKey: string,
host: string,
): Promise<Response> {
try {
const token = await getShopifySessionToken(apiKey, host);
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...options.headers,
};
return fetch(url, {
...options,
headers,
});
} catch (error) {
console.error("Error making authenticated request:", error);
throw error;
}
}
API Route Structure
// src/app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
import { ApiVersion, shopifyApi } from "@shopify/shopify-api";
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
scopes: process.env.SHOPIFY_SCOPES?.split(",") || [
"read_products",
"write_products",
],
hostName: process.env.SHOPIFY_APP_URL!.replace(/^https?:\/\//, ""),
apiVersion: ApiVersion.July25,
isEmbeddedApp: true,
});
export async function GET(request: NextRequest) {
try {
const url = new URL(request.url);
const shop = url.searchParams.get("shop");
if (!shop) {
return NextResponse.json(
{ error: "Shop parameter is required" },
{ status: 400 },
);
}
// Implementation here
return NextResponse.json({ products: [] });
} catch (error) {
console.error("Products fetch error:", error);
return NextResponse.json(
{ error: "Failed to fetch products" },
{ status: 500 },
);
}
}
🎨 UI Component Patterns
Polaris Component Usage
// Always use Polaris components for consistency
import { Banner, Button, Card, Stack, Text } from "@shopify/polaris";
export function AccountConnection({
connected,
approved,
loading = false,
onConnect,
onDisconnect,
}: AccountConnectionProps) {
return (
<Card sectioned>
<Stack vertical spacing="loose">
<Text as="h2" variant="headingMd">
Cosmo Account Connection
</Text>
{!connected ? (
<>
<Text as="p" variant="bodyMd">
Connect your Cosmo account to start publishing products.
</Text>
<Button primary onClick={onConnect} loading={loading}>
Connect Account
</Button>
</>
) : (
<>
<Banner tone="success" title="Account Connected">
Your Cosmo account is successfully connected.
</Banner>
{!approved && (
<Banner tone="info" title="Approval Pending">
We're reviewing your account.
</Banner>
)}
<Stack distribution="trailing">
<Button onClick={onDisconnect}>Disconnect Account</Button>
</Stack>
</>
)}
</Stack>
</Card>
);
}
Form Handling
// Use Contextual Save Bar for forms
import { ContextualSaveBar } from "@shopify/app-bridge-react";
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
return (
<>
{hasUnsavedChanges && (
<ContextualSaveBar
message="Unsaved changes"
saveAction={{
onAction: handleSave,
loading: saving,
}}
discardAction={{
onAction: handleDiscard,
}}
/>
)}
{/* Form content */}
</>
);
🛒 Sales Channel Features
Product Publishing
// src/app/api/products/route.ts
export async function POST(request: NextRequest) {
const { shop, productIds, action } = await request.json();
const client = new shopify.clients.Rest({ session });
for (const productId of productIds) {
if (action === "publish") {
// Create product listing for sales channel
const response = await client.post({
path: "product_listings",
data: {
product_listing: {
product_id: parseInt(productId),
},
},
});
} else if (action === "unpublish") {
// Remove product listing
await client.delete({
path: `product_listings/${productId}`,
});
}
}
}
Cart Permalink Generation
// src/utils/cartPermalink.ts
export interface CartLine {
variantId: number;
quantity: number;
}
export function generateCartPermalink(
shopDomain: string,
lines: CartLine[],
): string {
if (lines.length === 0) {
throw new Error("Cart lines cannot be empty");
}
const cartItems = lines
.map((line) => `${line.variantId}:${line.quantity}`)
.join(",");
return `https://${shopDomain}/cart/${cartItems}`;
}
Billing Integration
// src/app/api/billing/route.ts
export async function POST(request: NextRequest) {
const { shop, plan } = await request.json();
const client = new shopify.clients.Rest({ session });
const response = await client.post({
path: "recurring_application_charges",
data: {
recurring_application_charge: {
name: "Cosmo Sales Channel",
price: plan.price,
return_url: `${process.env.SHOPIFY_APP_URL}/billing/return`,
test: process.env.NODE_ENV === "development",
},
},
});
}
🎭 Theme App Extensions
App Embed Block
<!-- extensions/cosmo-widget/src/AppEmbed.liquid -->
<div id="cosmo-widget" style="position: fixed; bottom: 20px; right: 20px; z-index: 9999;">
<!-- Widget content -->
</div>
{% schema %}
{
"name": "Cosmo AI Widget",
"target": "body",
"settings": [
{
"type": "text",
"id": "widget_title",
"label": "Widget Title",
"default": "Cosmo AI Assistant"
}
]
}
{% endschema %}
Extension Configuration
# extensions/cosmo-widget/shopify.extension.toml
name = "cosmo-widget"
type = "app_embed"
[build]
command = ""
watch = []
[ui]
enable_create = true
paths = { preview = "/preview" }
🔄 Webhook Handling
Webhook Processing
// src/app/api/webhooks/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.text();
const hmac = request.headers.get("X-Shopify-Hmac-Sha256");
const topic = request.headers.get("X-Shopify-Topic");
const shop = request.headers.get("X-Shopify-Shop-Domain");
// Verify webhook authenticity
const isValid = shopify.webhooks.validate({
rawBody: body,
rawRequest: request,
rawResponse: new Response(),
});
if (!isValid) {
return NextResponse.json(
{ error: "Invalid webhook signature" },
{ status: 401 },
);
}
const data = JSON.parse(body);
// Handle different webhook topics
switch (topic) {
case "products/create":
await handleProductCreate(data, shop);
break;
case "app/uninstalled":
await handleAppUninstalled(data, shop);
break;
default:
console.log(`Unhandled webhook topic: ${topic}`);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Webhook processing error:", error);
return NextResponse.json(
{ error: "Webhook processing failed" },
{ status: 500 },
);
}
}
🚀 Performance Optimization
Code Splitting
// Lazy load non-critical components
import dynamic from "next/dynamic";
const ProductPublishing = dynamic(
() => import("../components/ProductPublishing"),
{ loading: () => <Spinner /> },
);
Image Optimization
// Use Next.js Image component
import Image from "next/image";
<Image
src="/product-image.jpg"
alt="Product image"
width={300}
height={200}
priority={false}
/>;
Bundle Optimization
// Use tree shaking for Polaris
import { Button, Card, Text } from "@shopify/polaris";
// Instead of: import * as Polaris from '@shopify/polaris';
🛡️ Security Best Practices
Environment Variables
# .env.local
SHOPIFY_API_KEY=your_api_key_here
SHOPIFY_API_SECRET=your_api_secret_here
SHOPIFY_APP_URL=https://your-app-url.com
SHOPIFY_SCOPES=read_products,write_products,read_orders,write_orders
Session Management
// Always validate session tokens
export function verifySessionToken(
token: string,
apiSecret: string,
): any | null {
try {
// Implement JWT verification
const decoded = shopify.session.decodeSessionToken(token);
return decoded;
} catch (error) {
console.error("Error verifying session token:", error);
return null;
}
}
📊 Testing Guidelines
Component Testing
import { render, screen } from "@testing-library/react";
import { AccountConnection } from "../AccountConnection";
describe("AccountConnection", () => {
it("renders connect button when not connected", () => {
render(
<AccountConnection
connected={false}
approved={false}
onConnect={jest.fn()}
/>,
);
expect(screen.getByText("Connect Account")).toBeInTheDocument();
});
});
API Testing
import { NextRequest } from "next/server";
import { GET } from "../route";
describe("/api/products", () => {
it("returns products for valid shop", async () => {
const request = new NextRequest(
"http://localhost/api/products?shop=test.myshopify.com",
);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.products).toBeDefined();
});
});
🚀 Deployment Checklist
Pre-Deployment
- [ ] Environment variables configured
- [ ] SSL certificates installed
- [ ] Webhook endpoints registered
- [ ] App URL updated in Partner Dashboard
- [ ] Performance metrics tested
Post-Deployment
- [ ] OAuth flow tested end-to-end
- [ ] Webhook delivery verified
- [ ] Product publishing tested
- [ ] Cart permalinks validated
- [ ] Theme extension working
📚 Resources
- Shopify App Development
- Sales Channel Apps
- App Bridge React
- Polaris Design System
- Next.js Documentation
Note: Always refer to the latest Shopify documentation for the most current requirements and best practices.