Custom Checkout Step Module
Module Structure
app/code/Vendor/CustomCheckout/
├── registration.php
├── etc/
│ ├── module.xml
│ ├── di.xml
│ └── frontend/
│ └── routes.xml
├── view/
│ └── frontend/
│ ├── layout/
│ │ └── checkout_index_index.xml
│ ├── web/
│ │ ├── js/
│ │ │ ├── view/
│ │ │ │ ├── custom-step.js
│ │ │ │ ├── gift-message.js
│ │ │ │ └── processor.js
│ │ │ └── mage/
│ │ │ └── validation.js
│ │ └── template/
│ │ ├── custom-step.html
│ │ └── gift-message.html
│ └── requirejs-config.js
├── Block/
│ └── Checkout/
│ └── CustomConfigProvider.php
└── Plugin/
└── Checkout/
└── LayoutProcessorPlugin.php
registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_CustomCheckout',
__DIR__
);
Layout XML - Add Custom Step
<!-- view/frontend/layout/checkout_index_index.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="custom-step" xsi:type="array">
<item name="component" xsi:type="string">Vendor_CustomCheckout/js/view/custom-step</item>
<item name="sortOrder" xsi:type="string">250</item>
<item name="children" xsi:type="array">
<item name="custom-step-view" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="sortOrder" xsi:type="string">100</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
This module adds a custom "Gift Options" step to checkout using KnockoutJS components and layout XML configuration.
KnockoutJS Components
Custom Step Component
// js/view/custom-step.js
define([
'jquery',
'ko',
'uiComponent',
'Magento_Checkout/js/model/step-navigator',
'Magento_Checkout/js/model/checkout-data-resolver',
'mage/translate'
], function ($, ko, Component, stepNavigator, checkoutDataResolver, $t) {
'use strict';
return Component.extend({
defaults: {
template: 'Vendor_CustomCheckout/custom-step'
},
initialize: function () {
this._super();
stepNavigator.registerStep(
'custom-step',
'custom-step',
$t('Gift Options'),
this.isVisible,
ko.observable(250),
'shipping'
);
checkoutDataResolver.resolveEstimationMethod();
return this;
},
isVisible: ko.observable(true),
navigate: function () {
this.isVisible(true);
},
stepCode: 'custom-step',
stepTitle: $t('Gift Options'),
isSelected: ko.observable(false),
isEnabled: ko.observable(true),
validate: function () {
return true;
},
getFormArguments: function () {
return {
timeout: 5000
};
},
giftMessage: ko.observable(''),
includeGiftReceipt: ko.observable(false),
includeGiftMessage: ko.observable(false),
giftRecipientName: ko.observable(''),
giftSenderName: ko.observable(''),
hasGiftMessage: function () {
return this.includeGiftMessage();
},
getGiftData: function () {
return {
gift_message: this.giftMessage(),
include_gift_receipt: this.includeGiftReceipt(),
include_gift_message: this.includeGiftMessage(),
recipient_name: this.giftRecipientName(),
sender_name: this.giftSenderName()
};
}
});
});
Custom Step Template
<!-- template/custom-step.html -->
<div class="custom-checkout-step" data-bind="visible: isVisible()">
<h2 data-bind="text: stepTitle"></h2>
<div class="gift-options">
<label class="gift-toggle">
<input type="checkbox" data-bind="checked: includeGiftMessage" />
<span>Include Gift Message</span>
</label>
<div data-bind="visible: hasGiftMessage">
<div class="field">
<label>Recipient Name</label>
<input type="text" data-bind="value: giftRecipientName" />
</div>
<div class="field">
<label>Sender Name</label>
<input type="text" data-bind="value: giftSenderName" />
</div>
<div class="field">
<label>Gift Message</label>
<textarea data-bind="value: giftMessage, attr: { maxlength: 255 }"></textarea>
</div>
</div>
<label class="gift-receipt">
<input type="checkbox" data-bind="checked: includeGiftReceipt" />
<span>Include Gift Receipt (hide prices)</span>
</label>
</div>
</div>
KnockoutJS observables bind form fields to the component. The stepNavigator controls step visibility and navigation flow.
Checkout Plugin and Data Provider
Layout Processor Plugin
<?php
namespace Vendor\CustomCheckout\Plugin\Checkout;
class LayoutProcessorPlugin
{
public function process(
\Magento\Checkout\Block\Checkout\LayoutProcessor $subject,
array $jsLayout
): array {
// Add custom fields to shipping step
$jsLayout['components']['checkout']['children']['shipping']['children']['shipping-address-fieldset']['children']['custom_shipping_note'] = [
'component' => 'Magento_Ui/js/form/element/textarea',
'config' => [
'customScope' => 'shippingAddress',
'template' => 'ui/form/field',
'elementTmpl' => 'ui/form/textarea',
],
'dataScope' => 'shippingAddress.custom_shipping_note',
'label' => __('Delivery Instructions'),
'provider' => 'checkoutProvider',
'validation' => [
'max-text-length' => 255,
],
];
// Add custom fields to billing step
$jsLayout['components']['checkout']['children']['billing-step']['children']['payment']['children']['payments-list']['children']['before-place-order']['children']['custom_note'] = [
'component' => 'Magento_Ui/js/form/element/textarea',
'config' => [
'customScope' => 'billingAddress',
'template' => 'ui/form/field',
'elementTmpl' => 'ui/form/textarea',
],
'dataScope' => 'billingAddress.custom_note',
'label' => __('Order Note'),
'provider' => 'checkoutProvider',
];
return $jsLayout;
}
}
Config Provider
<?php
namespace Vendor\CustomCheckout\Block\Checkout;
class CustomConfigProvider implements \Magento\Checkout\Model\ConfigProviderInterface
{
public function getConfig(): array
{
return [
'custom_checkout' => [
'enabled' => true,
'max_gift_message_length' => 255,
'allow_gift_receipt' => true,
'allow_delivery_instructions' => true,
],
];
}
}
The plugin modifies checkout layout at runtime. ConfigProvider supplies frontend configuration via JSON.
Testing and Validation
Checkout Flow Test
<?php
namespace Vendor\CustomCheckout\Test\Integration\Checkout;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
class CustomCheckoutTest extends TestCase
{
private $objectManager;
private $session;
protected function setUp(): void
{
$this->objectManager = Bootstrap::getObjectManager();
$this->session = $this->objectManager->get(
\Magento\Checkout\Model\Session::class
);
}
public function testCustomStepConfiguration(): void
{
$configProvider = $this->objectManager->create(
\Vendor\CustomCheckout\Block\Checkout\CustomConfigProvider::class
);
$config = $configProvider->getConfig();
$this->assertArrayHasKey('custom_checkout', $config);
$this->assertTrue($config['custom_checkout']['enabled']);
$this->assertEquals(255, $config['custom_checkout']['max_gift_message_length']);
}
public function testGiftMessageValidation(): void
{
$giftMessage = [
'enable_gift_message' => true,
'gift_message' => 'Happy Birthday!',
'recipient_name' => 'John Doe',
'sender_name' => 'Jane Doe',
];
$this->assertNotEmpty($giftMessage['gift_message']);
$this->assertNotEmpty($giftMessage['recipient_name']);
$this->assertNotEmpty($giftMessage['sender_name']);
$this->assertLessThanOrEqual(255, strlen($giftMessage['gift_message']));
}
public function testEmptyGiftMessageValidation(): void
{
$giftMessage = [
'enable_gift_message' => false,
'gift_message' => '',
'recipient_name' => '',
'sender_name' => '',
];
if (!$giftMessage['enable_gift_message']) {
$this->assertEmpty($giftMessage['gift_message']);
}
}
}
Tests verify configuration, validation rules, and component behavior in isolation.
Business Context, Architecture, and Production Considerations
Business Requirements Context
Who is the Customer?
This checkout customization targets mid-market to enterprise e-commerce retailers who need differentiated checkout experiences. Typical customers include:
- Gift retailers requiring gift messaging, gift receipts, and scheduled delivery dates
- B2B suppliers needing PO number fields, custom shipping instructions, and tax-exempt checkout flows
- Luxury brands requiring white-glove delivery options, engraving personalization, or premium packaging selections
- Subscription businesses needing recurring delivery scheduling at checkout
What Problem Does This Solve?
- Cart abandonment: 69.8% of carts are abandoned; custom checkout steps addressing specific buyer needs (gift options, delivery preferences) reduce abandonment for targeted segments
- Customer experience gap: Default Magento checkout lacks industry-specific fields without customization
- Operational efficiency: Delivery instructions and gift data flow directly to fulfillment, reducing customer service calls by 15-25%
- Revenue increase: Gift receipt and premium packaging options can add 3-7% to average order value
Architecture Decisions
Decision 1: KnockoutJS vs Modern Framework
| Alternative | Pros | Cons | Decision |
|---|---|---|---|
| KnockoutJS (Magento default) | Native integration, no extra build step | Aging framework, smaller ecosystem | Selected - lowest risk for checkout, avoids payment provider compatibility issues |
| React/Vue checkout replacement | Modern DX, better performance | Breaks payment integrations, requires full rebuild | Rejected for this scope |
| Web Components | Framework-agnostic, encapsulated | Magento 2 limited support, browser compatibility | Rejected |
Decision 2: Plugin vs Observer for Layout Modification
| Alternative | Pros | Cons | Decision |
|---|---|---|---|
| LayoutProcessorPlugin | Direct access to $jsLayout, chainable | Tightly coupled to checkout block | Selected - standard Magento pattern for checkout customization |
| Event observer (checkout_layout_load_after) | Loose coupling | Cannot chain, other modules may override | Rejected |
| Preference override | Full control | Breaks upgrade path, maintenance burden | Rejected |
Decision 3: Data Storage for Gift Messages
| Alternative | Pros | Cons | Decision |
| sales_order table extension | Atomic with order | Schema upgrade, migration needed | Selected - gift data is order-scoped |
| Separate gift_message table | Clean separation | Extra join, sync issues | Rejected |
| Magento_GiftMessage module | Already exists | Limited customization, module bloat | Rejected for custom fields |
Production Deployment Checklist
Pre-Deployment
- Run
setup:upgradein maintenance mode - Verify database schema changes apply cleanly
- Run full checkout integration test suite
- Test with all enabled payment methods (Stripe, PayPal, Braintree)
- Verify gift message data persists through order placement
- Test checkout with Redis and Varnish cache enabled
- Load test checkout flow at 2x peak traffic
- Verify CSP headers do not block custom JS
- Test with Magento single-use coupon codes
- Verify order email includes gift message data
Deployment
- Deploy via CI/CD pipeline (not manual)
- Enable maintenance mode during
setup:upgrade - Clear full page cache (Varnish + Magento)
- Invalidate compiled static files
- Run
setup:static-content:deployfor all locales - Verify static asset signing (asset pub/static/) works
Post-Deployment
- Smoke test: add product → gift options → place order
- Monitor error logs for 30 minutes
- Verify cron jobs related to orders still run
- Check order grid in admin for gift message display
- Validate gift message appears in order confirmation email
Monitoring and Alerting
Key Metrics to Track
Checkout Conversion Rate → Grafana dashboard
Gift Options Step Completion → Custom event in GTM
Checkout Error Rate → Magento exception log + Sentry
Average Checkout Duration → Custom timing metric
Payment Method Failure Rate → Payment gateway dashboards
Cart Abandonment Rate → Google Analytics enhanced e-commerce
Alerting Thresholds
| Metric | Warning | Critical | Action |
|---|---|---|---|
| Checkout error rate | > 1% | > 3% | Check logs, rollback if needed |
| Checkout duration | > 8s | > 15s | Investigate Redis/Varnish, check DB |
| Payment failure rate | > 2% | > 5% | Contact payment provider, check API keys |
| Order placement failures | > 5/hour | > 20/hour | Check inventory service, DB locks |
Logging Configuration
// app/etc/env.php - enable checkout logging
'logger' => [
'custom' => [
'channels' => [
'checkout' => [
'type' => 'file',
'filename' => 'checkout.log',
'max_files' => 10,
],
],
],
],
Cost Estimation
Development Cost
| Item | Hours | Rate | Cost |
|---|---|---|---|
| Custom checkout step (KnockoutJS) | 16 | $150/hr | $2,400 |
| Gift message data model + plugin | 12 | $150/hr | $1,800 |
| Payment method integration | 20 | $150/hr | $3,000 |
| Testing (unit + integration) | 8 | $150/hr | $1,200 |
| Code review + QA | 6 | $150/hr | $900 |
| Total Development | 62 | $9,300 |
Infrastructure Cost (Monthly)
| Component | Cost |
|---|---|
| Additional Redis memory (gift data cache) | $0 (shared) |
| Database storage (gift_message column) | ~$5/mo |
| Monitoring (Datadog/Grafana) | $23-70/mo |
| Error tracking (Sentry) | $26/mo |
| Total Monthly | ~$54-101/mo |
Ongoing Maintenance
| Item | Frequency | Cost |
|---|---|---|
| Magento version compatibility testing | Quarterly | $500 |
| Checkout conversion rate optimization | Monthly | $1,000 |
| Payment gateway updates | As needed | $300-800 |
| Annual Maintenance | $7,600-$14,600 |
Quiz
1. How do you add a custom step to checkout?
2. What plugin modifies checkout layout in Magento 2?
3. What JavaScript framework does Magento 2 checkout use?
4. Why was KnockoutJS chosen over React for checkout customization?
5. What is a critical post-deployment check for checkout changes?
Flashcards
Question
How do you add a checkout step?
Click to reveal answer
Answer
stepNavigator.registerStep() + layout XML + JS component
Question
What does LayoutProcessorPlugin do?
Click to reveal answer
Answer
Modifies checkout layout to add custom fields
Question
What JS framework powers checkout?
Click to reveal answer
Answer
KnockoutJS with uiComponent
Question
How do you validate checkout fields?
Click to reveal answer
Answer
use 'validation' config in UI component or jQuery validator
Question
Where are checkout templates?
Click to reveal answer
Answer
view/frontend/web/template/
Question
Why prefer KnockoutJS over React for checkout?
Click to reveal answer
Answer
Native integration avoids breaking payment provider compatibility
Question
What checkout metric should trigger an alert?
Click to reveal answer
Answer
Error rate > 1% warning, > 3% critical
Question
What is the main business value of checkout customization?
Click to reveal answer
Answer
Reduces cart abandonment and increases average order value
Revision Notes
Key Takeaways
- 1. Custom checkout steps use stepNavigator.registerStep()
- 2. Layout XML defines component hierarchy and sort order
- 3. LayoutProcessorPlugin adds custom fields to existing steps
- 4. KnockoutJS provides data binding for checkout components
- 5. ConfigProviderInterface supplies configuration to frontend
- 6. Checkout customization should target specific customer segments (gift retailers, B2B, luxury)
- 7. KnockoutJS is preferred over React to avoid payment integration breakage
- 8. Post-deployment requires full checkout flow smoke testing
- 9. Monitor checkout error rate and duration as critical health metrics
Interview Tips
- • Explain the checkout step registration flow
- • Describe how to add fields to shipping/billing forms
- • Discuss checkout validation patterns (client + server)
- • Talk about payment method integration in checkout
- • Explain why KnockoutJS is preferred over React for checkout changes
- • Discuss monitoring strategy for checkout in production
Cheat Sheet
Custom Checkout Step:
Register: stepNavigator.registerStep(code, title, sortOrder)
Component: extend uiComponent with template
Layout: add under checkout > steps > children
LayoutProcessorPlugin:
Intercept: Magento\Checkout\Block\Checkout\LayoutProcessor
Add fields to shippingAddress or billingStep
ConfigProvider:
Implements ConfigProviderInterface
getConfig() returns array for frontend
Gift Message:
js/view/gift-message.js → Component
template/gift-message.html → Template
Plugin saves to order on placeOrder
Business Context:
Target: gift retailers, B2B, luxury brands
Value: reduce abandonment, increase AOV
Risk: payment integration breakage → use KnockoutJS
Production:
Pre-deploy: integration tests, load tests, CSP check
Deploy: maintenance mode → setup:upgrade → cache clear
Post-deploy: smoke test, 30-min log monitoring
Monitoring:
Checkout error rate (warn >1%, crit >3%)
Checkout duration (warn >8s, crit >15s)
Payment failure rate (warn >2%, crit >5%)
Cost:
Development: ~$9,300 (62 hours)
Monthly infra: ~$54-101
Annual maintenance: $7,600-$14,600