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>
  );
}
// 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}`,
      });
    }
  }
}
// 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


Note: Always refer to the latest Shopify documentation for the most current requirements and best practices.