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
Target Location 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
Target Location 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
Target Location 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
Target Location analyticsMain analytics dashboard sales-analyticsSales analytics page inventoryInventory page
Other
Target Location 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:
Parameter Description Example hostEncoded host for App Bridge bXlzdG9yZS5sYXVuY2hteXN0b3JlLmlvproductIdProduct ID (on product pages) prod_abc123orderIdOrder ID (on order pages) order_xyz789customerIdCustomer ID (on customer pages) cust_def456collectionIdCollection ID (on collection pages) col_ghi012localeAdmin locale en-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
}
Navigate
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 , 8 px );
padding : var ( --lms-admin-spacing , 16 px );
}
.panel-header {
border-bottom : 1 px solid var ( --lms-admin-border-color , #e1e3e5 );
padding-bottom : var ( --lms-admin-spacing , 16 px );
margin-bottom : var ( --lms-admin-spacing , 16 px );
}
button {
background : var ( --lms-admin-primary-color , #008060 );
color : white ;
border : none ;
border-radius : var ( --lms-admin-border-radius , 4 px );
padding : 8 px 16 px ;
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
Always verify session tokens
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