The Shopify Subscriptions Reference App provides a comprehensive customer portal built as a Shopify Customer Account UI extension, allowing subscribers to manage their subscriptions directly.
Overview
The customer portal is a UI extension that appears in Shopify Customer Accounts, providing customers with self-service subscription management capabilities:
View Subscriptions Browse all active, paused, and canceled subscriptions
Manage Deliveries Update shipping addresses and delivery methods
Control Billing Pause, resume, skip, or cancel subscriptions
Update Payment Manage payment methods for recurring charges
The customer portal is implemented as a UI extension in extensions/buyer-subscriptions/ and integrates seamlessly with Shopify Customer Accounts.
Portal Structure
The customer portal consists of two main views:
Subscription List
Displays all subscriptions for the logged-in customer:
Active subscriptions with next billing date
Paused subscriptions with pause status
Canceled subscriptions with cancellation date
Quick status badges and pricing information
Subscription Details
Shows comprehensive information for a single subscription:
Upcoming order details and next billing date
Price breakdown and line items
Delivery information (shipping or pickup)
Past order history
Management actions (pause, resume, cancel, skip)
Router Implementation
The portal uses client-side routing in extensions/buyer-subscriptions/src/App.tsx:6-17:
export function Router () {
const currentEntry = useNavigationCurrentEntry ();
const url = new URL ( currentEntry . url );
if ( url . pathname . includes ( 'subscriptions' )) {
const id = getSubscriptionIdFromPath ( url . pathname );
return < SubscriptionDetails id ={ id } />;
}
return < SubscriptionList />;
}
Subscription Details Page
The details page provides a comprehensive view and management interface from extensions/buyer-subscriptions/src/SubscriptionDetails/SubscriptionDetails.tsx:32-183:
export function SubscriptionDetails ({ id } : SubscriptionDetailsProps ) {
const { data , loading , error , refetchSubscriptionContract } =
useSubscriptionContract ({ id });
const { i18n } = useExtensionApi ();
const { showSuccessToast } = useToast ();
const {
id : contractId ,
deliveryPolicy ,
lines ,
orders ,
shippingAddress ,
shippingMethodTitle ,
pickupAddress ,
priceBreakdownEstimate ,
status ,
upcomingBillingCycles ,
lastOrderPrice ,
lastBillingAttemptErrorType ,
} = data . subscriptionContract ;
const { nextBillingDate } = getBillingCycleInfo ( upcomingBillingCycles );
return (
< Page
title = { pageTitle }
primaryAction = {
< DetailsActions
contractId = { contractId }
status = { status }
refetchSubscriptionContract = { refetchSubscriptionContract }
lastOrderPrice = { lastOrderPrice }
nextOrderPrice = {priceBreakdownEstimate?. totalPrice }
nextBillingDate = { nextBillingDate }
/>
}
>
< Grid columns = { ... } rows = "auto" spacing = { [ 'loose' , 'loose' ]}>
<GridItem columnSpan={...}>
<BlockStack spacing="loose">
<UpcomingOrderCard {...} />
<OverviewCard {...} />
</BlockStack>
</GridItem>
<GridItem columnSpan={...}>
<BlockStack spacing="loose">
<PriceSummaryCard price={priceBreakdownEstimate} lines={lines} />
<PastOrdersCard orders={orders} />
</BlockStack>
</GridItem>
</Grid>
</Page>
);
}
Customer Actions
Customers can perform several actions on their subscriptions:
Pause Subscription
Access Subscription Details
Navigate to an active subscription in the customer portal
Click Pause Button
Select the pause action from the primary actions
Confirm Pause
Review the pause confirmation modal and confirm
Subscription Paused
The subscription status changes to PAUSED and billing stops
Implementation from extensions/buyer-subscriptions/src/SubscriptionDetails/DetailsActions/DetailsActions.tsx:20-44:
export function DetailsActions ({
contractId ,
status ,
refetchSubscriptionContract ,
} : DetailsActionsProps ) {
const { i18n } = useExtensionApi ();
const { showSuccessToast } = useToast ();
function onPauseSubscription () {
refetchSubscriptionContract ();
showSuccessToast ( SuccessToastType . Paused );
}
if ( status === 'ACTIVE' ) {
return (
< Button
overlay = {
< PauseSubscriptionModal
contractId = { contractId }
onPauseSubscription = { onPauseSubscription }
/>
}
>
< Text >{i18n.translate( 'subscriptionActions.pause' )}</Text>
</Button>
);
}
}
Pause actions initiated from the customer portal trigger the same backend services as admin-initiated pauses, ensuring consistency.
Resume Subscription
Customers can reactivate paused subscriptions:
Resume modal shows next billing date
Displays price comparison (last order vs. next order)
Confirms billing will restart immediately
{ status === 'PAUSED' ? (
< Button
overlay = {
< ResumeSubscriptionModal
contractId = { contractId }
resumeDate = { nextBillingDate }
lastOrderPrice = { lastOrderPrice }
nextOrderPrice = { nextOrderPrice }
onResumeSubscription = { onResumeSubscription }
/>
}
>
< Text >{i18n.translate( 'subscriptionActions.resume' )}</Text>
</Button>
) : null }
Cancel Subscription
Customers can permanently cancel their subscriptions:
Cancel modal explains the action is permanent
Shows final billing information
Requires explicit confirmation
< Button
overlay = {
< CancelSubscriptionModal
contractId = { contractId }
onCancelSubscription = { onCancelSubscription }
/>
}
>
< Text >{i18n.translate( 'cancel' )}</Text>
</Button>
Cancellations from the customer portal are permanent and cannot be undone. Consider offering a pause option before allowing cancellation.
Skip Next Order
Customers can skip upcoming billing cycles:
Available on the Upcoming Order Card
Skips the next scheduled billing date
Automatically schedules the following billing cycle
function onSkipOrder () {
showSuccessToast ( SuccessToastType . Skipped );
refetchSubscriptionContract ();
}
Delivery Management
Customers can update delivery information:
Update Shipping Address
Open Delivery Modal
Click edit on the Overview Card delivery section
Enter New Address
Fill in the new shipping address form
Select Delivery Method
Choose from available delivery methods for the new address
Confirm Changes
Review and save the address update
The address update flow:
< DeliveryModal
contractId = { contractId }
shippingAddress = { shippingAddress }
shippingMethodTitle = { shippingMethodTitle }
pickupAddress = { pickupAddress }
onDeliveryUpdate = { refetchSubscriptionContract }
/>
Delivery Method Selection
When updating addresses, customers can:
Choose standard shipping options
Select local pickup if available
View shipping costs for each method
See estimated delivery times
The portal automatically fetches available delivery methods for the new address, ensuring customers only see valid shipping options.
Upcoming Order Card
Displays details about the next scheduled order:
Next billing date and time
Product line items with images
Quantities and pricing
Skip order option
Inventory error warnings (if applicable)
< UpcomingOrderCard
onSkipOrder = { onSkipOrder }
refetchSubscriptionContract = { refetchSubscriptionContract }
refetchLoading = { refetchLoading }
contractId = { contractId }
upcomingBillingCycles = { upcomingBillingCycles }
hasInventoryError = { hasInventoryError }
/>
Past Orders Card
Shows history of completed orders:
Order date and order number
Total amount charged
Link to order status page
Payment status
< PastOrdersCard orders = { orders } />
Price Summary Card
Breaks down subscription pricing:
Line item subtotals
Discounts applied
Shipping costs
Taxes
Total amount
< PriceSummaryCard
price = { priceBreakdownEstimate }
lines = { lines }
/>
Payment Method Management
Customers manage payment methods through Shopify’s native payment management:
View current payment method
Receive payment update emails from merchants
Update payment methods via secure Shopify-hosted pages
Receive confirmation of payment updates
Payment method updates use Shopify’s built-in customer payment management for security and PCI compliance.
Email Notifications
The customer portal integrates with email notifications in app/routes/customerAccount.emails.ts:14-56:
export async function action ({ request } : ActionFunctionArgs ) {
const { sessionToken , cors } =
await authenticate . public . customerAccount ( request );
const { sub : customerGid , dest : shopDomain } = sessionToken ;
const body = await request . json ();
const { admin_graphql_api_id , operationName } = body ;
if ([ 'PAUSE' , 'RESUME' ]. includes ( operationName )) {
jobs . enqueue (
new CustomerSendEmailJob ({
payload: {
admin_graphql_api_id ,
admin_graphql_api_customer_id: customerGid ,
emailTemplate:
operationName === 'PAUSE'
? 'SUBSCRIPTION_PAUSED'
: 'SUBSCRIPTION_RESUMED' ,
},
shop: shopDomain ,
}),
);
}
return cors ( json ({ status: 'success' }, { status: 200 }));
}
Customers receive emails for:
Subscription paused confirmation
Subscription resumed confirmation
Payment retry notifications
Payment failure warnings
Payment method update requests
Status Badges
Visual indicators for subscription status:
Subscription is active and billing normally. Shows next billing date prominently.
Subscription is temporarily paused. Shows pause status and resume option.
Subscription has been permanently canceled. Shows historical data only, no management actions available.
Error Handling
The customer portal gracefully handles various error conditions:
Inventory Errors
const hasInventoryError =
( lastBillingAttemptErrorType as BillingAttemptErrorType | null ) ===
BillingAttemptErrorType . InventoryError ;
When inventory issues occur:
Banner displays inventory error message
Customers can’t skip if already skipped due to inventory
Clear messaging about merchant restocking
Payment Errors
Failed payment banner on subscription details
Link to update payment method
Clear explanation of retry schedule
Loading States
function SubscriptionDetailsSkeleton () {
return (
< Page title = "" loading >
< Grid columns = { ... } spacing = { [ 'loose' , 'loose' ]}>
<GridItem columnSpan={...}>
<BlockStack spacing="loose">
<Card padding>
<SkeletonTextBlock />
</Card>
</BlockStack>
</GridItem>
</Grid>
</Page>
);
}
Responsive Design
The portal adapts to different screen sizes:
< Grid
columns = {Style.default( [ 'fill' ])
.when({viewportInlineSize: { min: 'medium' }}, [ 'fill' , 'fill' ])
. when ({ viewportInlineSize: { min: 'large' }}, [
'fill' , 'fill' , 'fill' , 'fill' , 'fill' , 'fill' ,
'fill' , 'fill' , 'fill' , 'fill' , 'fill' , 'fill' ,
])}
rows = "auto"
spacing = { [ 'loose' , 'loose' ]}
>
Mobile: Single column layout
Tablet: Two column layout
Desktop: Full grid layout with optimized spacing
Best Practices
Clear Action Labels
Use descriptive button labels and confirmation modals to prevent accidental actions
Provide Context
Show pricing, dates, and impacts before customers commit to changes
Graceful Errors
Display helpful error messages with clear resolution steps
Optimize Performance
Use loading states and skeleton screens for better perceived performance
Mobile-First Design
Ensure all actions are accessible and easy to use on mobile devices
Test the customer portal on actual mobile devices to ensure touch targets are appropriately sized and forms are easy to complete.