One of the most important aspects of any eCommerce project is the payment experience. B2B Commerce on Lightning Experience, built on the Salesforce Platform, provides a rich feature set with multiple options for integrating payments into the purchasing process. In this blog post, we’ll focus on how to build and integrate a custom payment solution for your B2B store using Apex, Lightning Web Components, and Flow.

Understanding the payment flow

You may have first-hand experience with a payment flow. When you’re visiting an online store and you click on “Pay,” you’ll often get directed to the vendor’s payment service provider. After you’ve paid, you’ll be redirected back to the vendor’s store. This is what we’ll build for B2B Commerce on Lightning Experience.

The first step is to integrate with the payment service provider of your choice, which offers an external and hosted payment page (HPP) where the buyer will get redirected to enter their payment details. On a high level, the payment flow works like this:

  1. Create a redirect to the hosted payment page.
  2. After the customer selects the appropriate payment method and enters the payment details on the HPP, enable a redirect to your B2B store.
  3. Handle the payment provider response, which can result in either a payment authorization or an error.
  4. Reflect the payment states on B2B Commerce in order to be able to provide them to an ERP or other system later on.

B2B Commerce on Lightning Experience provides a variety of checkout flows out-of-the box. For this blog post, we won’t focus on those specific flows. Instead, we’ll take an agnostic approach.

Access the hosted payment page with LWC and Apex

The first task is to direct the buyer to the hosted payment page. As you may have already guessed, it’s not just a simple Lightning Web Component that can accomplish this client-side. It also involves Apex, which talks to the payment provider and sends back the given results. The flow looks like this:

  1. The buyer initiates payment via a Lightning web component.
  2. The component invokes an Apex method.
  3. The Apex method communicates with the payment service provider and communicates back the results to the Lightning Web Component.

We’ll first take a look at the Apex method. For storing configurations and parameters for the hosted payment page, we are using Custom Metadata. They are referenced as MerchantConfig throughout the code.

The Apex method initializePaymentPage is an excerpt showing that a request is initiated to the payment service provider. Upon a successful response, the provider returns the URL for redirecting to the hosted payment page. In addition, the WebCart object is modified to indicate that the cart is currently in a redirect state. Upon returning to the checkout, this can be used to re-initiate the checkout in the correct state.

@AuraEnabled(Cacheable=false)
public static Map<String, String> initializePaymentPage(String cartId, String merchantConfig) {
    Map<String, String> resultMap = new  Map<String, String>();
        
    // Retrieves the Merchang config
    MerchantAccount__mdt config = getMerchantConfig(merchantConfig);
    Webcart cart = [SELECT Id,  GrandTotalAmount FROM Webcart WHERE Id =: cartId LIMIT 1];
        
    String currentRequestURL = URL.getCurrentRequestUrl().getProtocol(); + '://' + URL.getCurrentRequestUrl().getHost();
       
    // Creates a request for the payment provider
    // ....
    
    // Creates return urls for the payment provided
    returnUrls.Success = currentRequestURL + '/' + config.successURL__c  + '/'+ cartId;
    returnUrls.Fail = currentRequestURL + '/' + config.errorURL__c + '/?recordId='+ cartId  + '&paymentState=error'; 

    // Build the HTTP request for the payment provider
    // ...
    HttpResponse httpResponse = http.send(req);

    // Successful result store this as a card payment method
    if (httpResponse.getStatusCode() == 200) {
        // Desezerialize the response
        PaymentVO.InitPaymentResponse paymentMethodsResponse = (PaymentVO.InitPaymentResponse) JSON.deserialize(httpResponse.getBody(), PaymentVO.InitPaymentResponse.class);

        CardPaymentMethod cpm = new CardPaymentMethod();
        cpm.Status = 'Active';
        // Store further information on CardPaymentMethod
        insert cpm;

        cart.PaymentMethodId = cpm.ID;
        cart.CartHandling__c = 'Redirect';
        update cart;
        resultMap.put('Success', 'true');
            
        // Store the redirect URL for the LWC component
        resultMap.put('RedirectUrl', paymentMethodsResponse.RedirectUrl);
    } else {
       resultMap.put('Success', 'false');
    }
    return resultMap;
 }

The Lightning Web Component invokes the Apex method with the cart and merchant information. And in the case of a successful response, it utilizes the RedirectURL response value to redirect the client.

import { api, LightningElement } from 'lwc';
import initializePaymentPage from '@salesforce/apex/PaymentService.initializePaymentPage';

export default class PaymentExampleLWC extends LightningElement {
    @api cartId;
    @api merchantConfig;
    async initializePayment() {
        let paymentInitResponse = await initializePaymentPage({
            cartId: this.cartId,
            merchantConfig: this.merchantConfig
        });
        // we redirect to success URL (hosted payment page)
        if (paymentInitResponse.Success && paymentInitResponse.RedirectUrl) {
            window.location = paymentInitResponse.RedirectUrl;
        }
    }
}

Let’s see what this could look like (in a very simplified manner) using a Flow. If there is a no-redirect pending, the customer will need to be redirected to the hosted payment page for the first time. Otherwise, if there is already a pending redirect, we can safely assume that the buyer has been redirected from the payment page back to the B2B store. In that case, we can process the checkout transaction for order creation.

Now that we looked at how to build a payment flow, let’s check out how to authorize your payments using a payment gateway adapter.

Authorize payments using a payment gateway adapter

To take care of authorizing the payment call, we’ll use a payment gateway adapter (which we will look at in more detail later on). First, we need to set up the payment gateway for your store following the Payment Setup instructions.

Payment gateway: the glue for our integration

Payment gateway adapters represent the integration between your payment platform in Salesforce and an external payment gateway (see B2B payment documentation). For our needs, the payment gateway is the central point on the Salesforce Platform for integrating with any kind of payment provider. The B2B LE Quickstart repository provides good examples on how to write your own payment gateway adapter. We recommend checking out those when building your own implementation as they provide a good starting point.

Payment gateway invocation and context

As we want to integrate the communication with the payment gateway into our flow, we’ll first create a new InvocableMethod. This method fetches the relevant gateway information and initiates the authorization with the gateway using the Payment API.

public class AuthorizeTokenizedPayment {

    @InvocableMethod(callout=true label='Authorize Tokenized Payment' description='Authorizes payment for credit information that was previously tokenized' category='Commerce')
    public static List<String> authorizePaymentInfo(List<AuthorizeTokenizedPaymentRequest> request) {
        String cartId = request[0].cartId;
        WebCart cart = [SELECT WebStoreId, GrandTotalAmount, AccountId, PaymentMethodId
                        FROM WebCart WHERE Id=:cartId];
        
        String paymentGatewayId = [SELECT Integration FROM StoreIntegratedService WHERE ServiceProviderType='Payment' AND StoreId=:cart.WebStoreId].Integration;
        ConnectApi.AuthorizationRequest authRequest = new ConnectApi.AuthorizationRequest();
        ConnectApi.AuthApiPaymentMethodRequest authApiPaymentMethodRequest = new ConnectApi.AuthApiPaymentMethodRequest();
        authApiPaymentMethodRequest.Id = cart.PaymentMethodId;
        authRequest.accountId = cart.AccountId;
        authRequest.amount = cart.GrandTotalAmount;
    
        authRequest.paymentGatewayId = paymentGatewayId;
        authRequest.paymentMethod = authApiPaymentMethodRequest;
        authRequest.paymentGroup = getPaymentGroup(cartId);

        // Call processRequest on the paymentGatewayAdapter 
        ConnectApi.AuthorizationResponse authResponse = ConnectApi.Payments.authorize(authRequest)

        // .... 
    }
}

The Payments API provides several methods, such as authorize or capture, as you can see with the invocation of ConnectApi.Payments.authorize(authRequest) in the previous code snippet. Whenever one of the Payments API methods is invoked, it automatically calls a processRequest method. As we implemented our own payment gateway, we have to provide that method ourselves.

The following simplified examples shows how you can implement the processRequest. On invocation, the context of the selected payment gateway is passed via the PaymentGatewayContext object.

global with sharing class MyPaymentGatewayAdapter implements CommercePayments.PaymentGatewayAdapter {
    
     global CommercePayments.GatewayResponse processRequest(CommercePayments.paymentGatewayContext gatewayContext) {
        CommercePayments.RequestType requestType = gatewayContext.getPaymentRequestType();
        if (requestType == CommercePayments.RequestType.Authorize) {
           // Handle authorize
        } else if (requestType == CommercePayments.RequestType.Capture) {
           // Handle capture
        } else {
           // Other request types
        }
        return null;
    }
}

The following code provides an example of how to create an authorization with the payment service provider of your choice. You would call it when the payment request type is CommercePayments.RequestType.Authorize as shown in the previous example.

It’s important to understand that we are interacting within the payment gateway context, and therefore, we provide out-of-the-box a special HttpCallout functionality that’s designed for payment gateways: CommercePayments.PaymentsHttp(). This automatically references the named credentials attached to your PaymentGatewayProvider. Furthermore, it creates the PaymentAuthorization sObject and attaches all the details needed.

private static CommercePayments.GatewayResponse handleAuthorization(CommercePayments.paymentGatewayContext gatewayContext) {

    CommercePayments.AuthorizationRequest commerceAuthRequest = (CommercePayments.AuthorizationRequest)gatewayContext.getPaymentRequest();
    CommercePayments.AuthApiPaymentMethodRequest authPaymentMethod = commerceAuthRequest.paymentMethod;
    String paymentMethodId = authPaymentMethod.Id;
    CardPaymentMethod paymentMethod = [SELECT ID, GatewayToken, PaymentConfiguration__c FROM CardPaymentMethod WHERE ID =: paymentMethodId];
    HttpRequest req = PaymentUtils.buildRequest(paymentMethod);
    CommercePayments.PaymentsHttp http = new commercepayments.PaymentsHttp();
    HttpResponse res = http.send(req);

    if (res.getStatusCode() == 200) {
        CommercePayments.AuthorizationResponse commerceResponse = new CommercePayments.AuthorizationResponse();
        PaymentVO.PaymentAssertResponse paymentAssertResponse = (PaymentVO.PaymentAssertResponse) JSON.deserialize(resBody, PaymentVO.PaymentAssertResponse.class);
        commercepayments.PaymentMethodTokenizationResponse paymentMethodTokenizationResponse = new CommercePayments.PaymentMethodTokenizationResponse();
        commerceResponse.setGatewayResultCode(paymentAssertResponse.Transaction_x.ApprovalCode);
        commerceResponse.setGatewayReferenceNumber(paymentAssertResponse.Transaction_x.Id);
        commerceResponse.setSalesforceResultCodeInfo(SUCCESS_SALESFORCE_RESULT_CODE_INFO);
        return commerceResponse;
    } else {
        CommercePayments.AuthorizationResponse commerceResponse = new CommercePayments.AuthorizationResponse();
        PaymentVO.PaymentErrorResponse paymentErrorResponse = (PaymentVO.PaymentErrorResponse) JSON.deserialize(res.getBody(), PaymentVO.PaymentErrorResponse.class);
        String errorName = paymentErrorResponse.ErrorName;
        CommerceResponse.setSalesforceResultCodeInfo(DECLINE_SALESFORCE_RESULT_CODE_INFO);
        CommercePayments.GatewayErrorResponse error = new CommercePayments.GatewayErrorResponse(String.valueOf(res.getStatusCode()), paymentErrorResponse.ErrorName);
        return error;  
    }
}

Coming back to our simplified flow, we can then check, depending on the cart handling, whether we need to call the redirect to the hosted payments page or whether we can proceed with the payment authorization.

Dealing with payment states and create custom redirects

With payment handling, one of the things to consider is a non-successful payment response. As you may have noticed throughout this article, we provide an error URL for the payment service provider.

returnUrls.Fail = currentRequestURL + '/' + config.errorURL__c + '/?recordId='+ cartId  + '&paymentState=error';

Let’s make it more real and redirect the customer back to a cart page after the payment was denied. Our error URL would then look like this:

returnUrls.Fail = 'https://<yourdomain>/MyShop/s/<name of the experience page>/<name of the flow> + '/?recordId='+ cartId  + '&paymentState=error';

To display the cart page, we create a new flow (in this case a screen flow). This flow will handle the errors/non-successful payment responses that can happen during the payment transaction.

You’ll see that we provide a redirect screen at the end of the flow. In this screen, we are embedding another custom Lightning Web Component. The JavaScript code below shows that this component is sending the customer to whatever the appropriate error result page will be (in our case, the cart page).

import { LightningElement, api } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';

export default class PaymentRedirect extends NavigationMixin(LightningElement) {

    @api recordId;
    @api pageName;
    @api objectApiName; 

    connectedCallback() {
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: this.recordId,
                objectApiName: this.objectApiName,
                actionName: 'view'
            },
            state: {
                paymentState: 'error'
            }
        });
    }
}

To trigger the flow, which starts when receiving an error from the payment service provider, we’ll need to provide a common entry point. Therefore, we are adding the created screen flow to a newly created “Redirect” page in Experience Builder. As soon as the page is loaded, the flow is initiated.

As you can customize URLs for experience sites, you can create nice, human readable URLs, like this one:

https://<your url>/MyShop/s/<name of the experience page>/<name of the flow>

Conclusion

Integrating payments into our B2B store is really straightforward with B2B Commerce on Lightning Experience tools. When using the provided APIs, the platform takes care of providing all relevant sObjects and attaches them throughout the checkout process. Payment gateways provide a powerful framework that allows you to seamlessly hook your payments into the storefront payment process and lets you execute them in whichever checkout step needed. By combining Apex, Lightning Web Components, and Flow, you can go beyond the default provided mechanism and tailor the payment experience to your organizations requirements.

About the author

Christoph Hallmann is a Senior Technical Architect on the Professional Service Delivery team. He’s responsible for implementations with Salesforce B2B/B2C Commerce Cloud and Salesforce order management.

Get the latest Salesforce Developer blog posts and podcast episodes via Slack or RSS.

Add to Slack Subscribe to RSS