Skip to content
advanced Phase 118 · Advanced Projects

Project - Custom Checkout Feature

Build a custom checkout feature with custom step, payment integration, shipping customization, and order processing

2h
0 problems
Topic Progress 0%

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?

  1. Cart abandonment: 69.8% of carts are abandoned; custom checkout steps addressing specific buyer needs (gift options, delivery preferences) reduce abandonment for targeted segments
  2. Customer experience gap: Default Magento checkout lacks industry-specific fields without customization
  3. Operational efficiency: Delivery instructions and gift data flow directly to fulfillment, reducing customer service calls by 15-25%
  4. 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:upgrade in 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:deploy for 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?

Question 1 options

2. What plugin modifies checkout layout in Magento 2?

Question 2 options

3. What JavaScript framework does Magento 2 checkout use?

Question 3 options

4. Why was KnockoutJS chosen over React for checkout customization?

Question 4 options

5. What is a critical post-deployment check for checkout changes?

Question 5 options

Flashcards

Question

How do you add a checkout step?

Answer

stepNavigator.registerStep() + layout XML + JS component

Question

What does LayoutProcessorPlugin do?

Answer

Modifies checkout layout to add custom fields

Question

What JS framework powers checkout?

Answer

KnockoutJS with uiComponent

Question

How do you validate checkout fields?

Answer

use 'validation' config in UI component or jQuery validator

Question

Where are checkout templates?

Answer

view/frontend/web/template/

Question

Why prefer KnockoutJS over React for checkout?

Answer

Native integration avoids breaking payment provider compatibility

Question

What checkout metric should trigger an alert?

Answer

Error rate > 1% warning, > 3% critical

Question

What is the main business value of checkout customization?

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