Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt

Use this file to discover all available pages before exploring further.

Admin Block Extensions

Admin blocks add custom UI panels to the merchant’s LaunchMyStore admin dashboard. Use them to display app data, add quick actions, or integrate with external services directly in the admin interface.

How Admin Blocks Work

Admin blocks are rendered as iframes within the merchant admin. They communicate with the host using App Bridge, allowing them to access admin data and trigger actions.
┌──────────────────────────────────────────────────────────────┐
│  LaunchMyStore Admin                                         │
├──────────────────────────────────────────────────────────────┤
│  Product Details                                             │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Product Title                                         │  │
│  │  [Native admin content]                                │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Your App Block (iframe)                                     │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  [Your custom UI]                                      │  │
│  │  - Product reviews summary                             │  │
│  │  - Quick actions                                       │  │
│  │  - External data                                       │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Available Targets

Admin blocks can be placed at 26 injection points:

Resource Detail Pages

TargetLocation
product.details.blockProduct detail page
order.details.blockOrder detail page
customer.details.blockCustomer detail page
collection.details.blockCollection detail page
discount.details.blockDiscount detail page
draft-order.details.blockDraft order detail page
blog.details.blockBlog detail page
article.details.blockBlog article detail page
page.details.blockPage detail page

List Pages

TargetLocation
product-listProducts list page
order-listOrders list page
customer-listCustomers list page
collection-listCollections list page
abandoned-order-listAbandoned checkouts page
gift-card-listGift cards list page
blog-listBlogs list page
contact-listContact form submissions

Settings Pages

TargetLocation
shipping-settingsShipping settings page
payment-settingsPayment settings page
tax-settingsTax settings page
checkout-settingsCheckout settings page
pos-settingsPOS settings page
account-settingsAccount settings page

Analytics

TargetLocation
analyticsMain analytics dashboard
sales-analyticsSales analytics page
inventoryInventory page

Other

TargetLocation
order-createOrder creation page

Extension Manifest

Register admin blocks in your app.json:
{
  "handle": "my-reviews-app",
  "name": "Product Reviews",
  "version": "1.0.0",
  "extensions": {
    "admin_blocks": [
      {
        "handle": "reviews-panel",
        "name": "Reviews Panel",
        "target": "product.details.block",
        "url": "https://my-app.com/admin/product-reviews"
      },
      {
        "handle": "reviews-summary",
        "name": "Reviews Summary",
        "target": "analytics",
        "url": "https://my-app.com/admin/reviews-analytics"
      }
    ]
  }
}

Creating an Admin Block

Admin blocks are web pages that run inside an iframe. Use App Bridge to communicate with the admin:
import { createApp } from '@launchmystore/app-bridge';
import { useEffect, useState } from 'react';

function ProductReviewsPanel() {
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState([]);
  const [loading, setLoading] = useState(true);
  
  // Initialize App Bridge
  const app = createApp({
    apiKey: process.env.NEXT_PUBLIC_APP_CLIENT_ID,
    host: new URLSearchParams(location.search).get('host')
  });
  
  useEffect(() => {
    // Get product ID from URL params
    const params = new URLSearchParams(location.search);
    const productId = params.get('productId');
    
    if (productId) {
      loadReviews(productId);
    }
  }, []);
  
  const loadReviews = async (productId) => {
    const token = await app.getSessionToken();
    
    const response = await fetch(`/api/reviews?productId=${productId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    const data = await response.json();
    setReviews(data.reviews);
    setProduct(data.product);
    setLoading(false);
  };
  
  const handleViewAll = () => {
    app.dispatch({
      type: 'REDIRECT',
      payload: {
        url: `/admin/apps/my-reviews-app/products/${product.id}`,
        newContext: false
      }
    });
  };
  
  const showToast = (message) => {
    app.dispatch({
      type: 'TOAST',
      payload: { message, duration: 3000 }
    });
  };
  
  if (loading) {
    return <div className="loading">Loading reviews...</div>;
  }
  
  return (
    <div className="reviews-panel">
      <div className="panel-header">
        <h3>Customer Reviews</h3>
        <button onClick={handleViewAll}>View All</button>
      </div>
      
      <div className="reviews-summary">
        <div className="stat">
          <span className="stat-value">{reviews.length}</span>
          <span className="stat-label">Total Reviews</span>
        </div>
        <div className="stat">
          <span className="stat-value">{calculateAverage(reviews)}</span>
          <span className="stat-label">Average Rating</span>
        </div>
      </div>
      
      <div className="recent-reviews">
        <h4>Recent Reviews</h4>
        {reviews.slice(0, 3).map((review) => (
          <div key={review.id} className="review-item">
            <div className="review-header">
              <span className="review-author">{review.author}</span>
              <span className="review-rating">{'★'.repeat(review.rating)}</span>
            </div>
            <p className="review-body">{review.body}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export default ProductReviewsPanel;

URL Parameters

Admin blocks receive context via URL query parameters:
ParameterDescriptionExample
hostEncoded host for App BridgebXlzdG9yZS5sYXVuY2hteXN0b3JlLmlv
productIdProduct ID (on product pages)prod_abc123
orderIdOrder ID (on order pages)order_xyz789
customerIdCustomer ID (on customer pages)cust_def456
collectionIdCollection ID (on collection pages)col_ghi012
localeAdmin localeen-US
// Extract context from URL
const params = new URLSearchParams(location.search);
const host = params.get('host');
const productId = params.get('productId');
const locale = params.get('locale');

Sizing and Layout

Fixed Height

By default, admin blocks have a fixed height. Set the height in your manifest:
{
  "handle": "reviews-panel",
  "target": "product.details.block",
  "url": "https://my-app.com/admin/product-reviews",
  "height": 400
}

Dynamic Height

For content with variable height, notify the host of size changes:
// After content loads or changes
app.dispatch({
  type: 'RESIZE',
  payload: {
    height: document.body.scrollHeight
  }
});
// React hook for auto-resizing
function useAutoResize(app) {
  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      const height = entries[0].contentRect.height;
      app.dispatch({
        type: 'RESIZE',
        payload: { height }
      });
    });
    
    observer.observe(document.body);
    return () => observer.disconnect();
  }, [app]);
}

App Bridge Actions

Admin blocks can use all App Bridge actions:

Show Toast

app.dispatch({
  type: 'TOAST',
  payload: {
    message: 'Review approved!',
    duration: 3000
  }
});

Open Modal

const result = await app.dispatchAndWait({
  type: 'MODAL_OPEN',
  payload: {
    title: 'Delete Review?',
    message: 'This action cannot be undone.',
    primaryAction: { content: 'Delete', destructive: true },
    secondaryAction: { content: 'Cancel' }
  }
});

if (result.action === 'primary') {
  // User clicked Delete
}
app.dispatch({
  type: 'REDIRECT',
  payload: {
    url: '/admin/products',
    newContext: false
  }
});

Resource Picker

const result = await app.dispatchAndWait({
  type: 'RESOURCE_PICKER',
  payload: {
    resourceType: 'product',
    multiple: true
  }
});

console.log(result.selection); // Selected products

Loading State

// Show loading bar
app.dispatch({ type: 'LOADING_START' });

// Hide loading bar
app.dispatch({ type: 'LOADING_STOP' });

Styling Guidelines

Admin blocks should match the LaunchMyStore admin design. Avoid custom colors that clash with the admin theme.

Use Admin CSS Variables

.reviews-panel {
  font-family: var(--lms-admin-font-family, -apple-system, BlinkMacSystemFont, sans-serif);
  color: var(--lms-admin-text-color, #202223);
  background: var(--lms-admin-surface-color, #ffffff);
  border-radius: var(--lms-admin-border-radius, 8px);
  padding: var(--lms-admin-spacing, 16px);
}

.panel-header {
  border-bottom: 1px solid var(--lms-admin-border-color, #e1e3e5);
  padding-bottom: var(--lms-admin-spacing, 16px);
  margin-bottom: var(--lms-admin-spacing, 16px);
}

button {
  background: var(--lms-admin-primary-color, #008060);
  color: white;
  border: none;
  border-radius: var(--lms-admin-border-radius, 4px);
  padding: 8px 16px;
  cursor: pointer;
}

button:hover {
  background: var(--lms-admin-primary-hover, #006e52);
}

Dark Mode Support

@media (prefers-color-scheme: dark) {
  .reviews-panel {
    --lms-admin-text-color: #e1e3e5;
    --lms-admin-surface-color: #1a1a1a;
    --lms-admin-border-color: #333;
  }
}

Example: Order Fulfillment Panel

A complete example showing order data and fulfillment actions:
import { createApp } from '@launchmystore/app-bridge';
import { useEffect, useState } from 'react';
import './fulfillment-panel.css';

function FulfillmentPanel() {
  const [order, setOrder] = useState(null);
  const [fulfillments, setFulfillments] = useState([]);
  const [loading, setLoading] = useState(true);
  const [syncing, setSyncing] = useState(false);
  
  const app = createApp({
    apiKey: process.env.NEXT_PUBLIC_APP_CLIENT_ID,
    host: new URLSearchParams(location.search).get('host')
  });
  
  useEffect(() => {
    const orderId = new URLSearchParams(location.search).get('orderId');
    if (orderId) loadOrderData(orderId);
  }, []);
  
  const loadOrderData = async (orderId) => {
    const token = await app.getSessionToken();
    
    const response = await fetch(`/api/fulfillment/order/${orderId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    const data = await response.json();
    setOrder(data.order);
    setFulfillments(data.fulfillments);
    setLoading(false);
  };
  
  const syncToWarehouse = async () => {
    setSyncing(true);
    app.dispatch({ type: 'LOADING_START' });
    
    try {
      const token = await app.getSessionToken();
      
      await fetch('/api/fulfillment/sync', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ orderId: order.id })
      });
      
      app.dispatch({
        type: 'TOAST',
        payload: { message: 'Order synced to warehouse!' }
      });
      
      loadOrderData(order.id);
    } catch (error) {
      app.dispatch({
        type: 'TOAST',
        payload: { message: 'Sync failed. Please try again.' }
      });
    } finally {
      setSyncing(false);
      app.dispatch({ type: 'LOADING_STOP' });
    }
  };
  
  const trackShipment = (trackingNumber) => {
    app.dispatch({
      type: 'REDIRECT',
      payload: {
        url: `https://track.example.com/${trackingNumber}`,
        external: true
      }
    });
  };
  
  if (loading) {
    return <div className="panel loading">Loading...</div>;
  }
  
  return (
    <div className="fulfillment-panel">
      <div className="panel-header">
        <h3>Warehouse Integration</h3>
        <span className={`status status--${order.warehouseStatus}`}>
          {order.warehouseStatus}
        </span>
      </div>
      
      <div className="panel-body">
        {fulfillments.length === 0 ? (
          <div className="empty-state">
            <p>Order not yet synced to warehouse</p>
            <button onClick={syncToWarehouse} disabled={syncing}>
              {syncing ? 'Syncing...' : 'Sync to Warehouse'}
            </button>
          </div>
        ) : (
          <div className="fulfillments">
            {fulfillments.map((fulfillment) => (
              <div key={fulfillment.id} className="fulfillment-card">
                <div className="fulfillment-header">
                  <span className="carrier">{fulfillment.carrier}</span>
                  <span className={`badge badge--${fulfillment.status}`}>
                    {fulfillment.status}
                  </span>
                </div>
                
                <div className="fulfillment-details">
                  <p>
                    <strong>Tracking:</strong> {fulfillment.trackingNumber}
                  </p>
                  <p>
                    <strong>Items:</strong> {fulfillment.itemCount}
                  </p>
                  <p>
                    <strong>Shipped:</strong> {fulfillment.shippedAt}
                  </p>
                </div>
                
                <button 
                  className="btn-secondary"
                  onClick={() => trackShipment(fulfillment.trackingNumber)}
                >
                  Track Shipment
                </button>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

export default FulfillmentPanel;

Security

Validate the session token on your backend before returning sensitive data.
Verify the merchant has granted required scopes before performing actions.
Never trust URL parameters. Validate and sanitize all inputs.
Admin blocks must be served over HTTPS. HTTP URLs will be blocked.

Debugging

Development Mode

Use localhost URLs during development:
{
  "handle": "reviews-panel",
  "url": "https://localhost:3000/admin/product-reviews"
}

Console Logging

App Bridge messages are logged in development:
if (process.env.NODE_ENV === 'development') {
  app.subscribe('*', (action) => {
    console.log('App Bridge:', action);
  });
}

Inspect Network

Use browser DevTools to inspect:
  • iframe loading
  • App Bridge postMessage events
  • API calls with session tokens