Implement a Front-Commerce payment method Choose the payment workflow wisely
Payment can be handled synchronously or asynchronously.
If Front-Commerce allows both strategies to work, we highly recommend you to
implement an asynchronous payment method (using
IPN )
whenever it is possible.
This will prevent your payments from being rejected later within the provider
process without your backend application knowing about it.
In this guide, we'll implement an asynchronous Front-Commerce payment method.
Implement the server logic
Front-Commerce allows you to implement your own payment method. New embedded
payment methods have to be registered from a GraphQL module. Let’s create a new
"PWAy" payment module for a fictive payment provider.
2 Register the payment method in the module’s contextEnhancer my-extension/runtime.ts
export default createGraphQLRuntime ( {
contextEnhancer : ( { loaders } ) => {
const loader = new MyPaymentLoader ( ) ;
const METHOD_CODE = "pway_awesomecheckout" ;
const METHOD_TITLE = "PWAy" ;
loaders . Payment . registerEmbeddedPaymentMethod (
METHOD_CODE ,
METHOD_TITLE ,
( paymentData , orderId , orderPaymentDetails ) => {
return loader . order ( paymentData , orderId , orderPaymentDetails ) ;
} ,
null ,
new MyPaymentNotificationProcessor ( hipayConfig )
) ;
return { } ;
} ,
} ) ;
3 Implement your loader's order method to process a payment my-extension/loaders/MyPaymentLoader.ts
export default class MyPaymentLoader {
constructor ( ) {
}
async order ( paymentData , orderId , orderPaymentDetails ) {
const paymentStatus = call ( ) ;
switch ( paymentStatus . status ) {
case SUCCESS :
return new PaymentAuthorized (
new PurchaseIdentifier ( { orderId } ) ,
paymentStatus . paymentReference
) ;
case REFUSED :
return new PaymentRefused (
new PurchaseIdentifier ( { orderId } ) ,
paymentStatus . reason
) ;
}
}
}
4 Implement the notification processor (for IPN handling) See
the existing DomainEvent classes to update the order .
See
the existing EarlyNotificationAck classes below
my-extension/loaders/MyPaymentLoader.ts
import {
AcknowledgeNotification ,
RefuseNotification ,
NotificationProcessor ,
type PaymentCommandDispatcher
} from "@front-commerce/core/graphql/payment" ;
export default class MyPaymentNotificationProcessor extends NotificationProcessor {
constructor ( ... ) {
...
}
async getEarlyNotificationAck ( notificationData : { notificationPayload : any } ) {
const data = notificationData . notificationPayload ;
if ( valid ) {
return new AcknowledgeNotification ( "Accepted" ) ;
} else {
return new RefuseNotification ( "Invalid notification" ) ;
}
}
async process ( notificationData : { status : string } , paymentCommandDispatcher : PaymentCommandDispatcher ) {
switch ( notificationData . status ) {
case "IN_PROGRESS" :
const paymentDetails = new PaymentDetails (
cartId ,
orderTotalAmount ,
orderCurrency
) ;
const orderId = await paymentCommandDispatcher . execute (
new PlaceOrderCommand (
paymentDetails ,
{
code : METHOD_CODE ,
additionalData : [
{ key : "transactionId" , value : transactionId } ,
] ,
} ,
guestCartId
)
) ;
return [
new PaymentCaptureStarted ( new PurchaseIdentifier ( { orderId } ) , {
transactionId ,
} ) ,
] ;
case "PAID" :
return [
new PaymentCaptured (
new PurchaseIdentifier ( { cartId } )
) ,
] ;
case "ERROR" :
return [ ... ] ;
}
}
}
Early Notification returns Early notifications can be used by notification processors to acknowledge or
reject a notification for security reasons (invalid authenticity proof).
AcknowledgeNotification When a notification is acknowledged the payment provider is answered with a
status 200 - OK
RefuseNotification When a notification is refused the payment provider is answered with a status
403 - Forbidden
Handle a sublist of payment methods
You may need to dynamically set the list of payment methods displayed for your
module.
This is achieved by registering a replacement handler
my-extension/runtime.ts
export default createGraphQLRuntime ( {
contextEnhancer : ( { loaders } ) => {
loaders . Payment . registerEmbeddedPaymentMethod (
) ;
loaders . Payment . registerMultiplePaymentMethods ( {
isMethodToReplace : ( method ) => method . code === METHOD_CODE ,
getReplacementMethods : async ( ) => {
try {
const paymentMethods = call ( ) ;
return paymentMethods . map ( ( method ) => ( {
code : ` ${ prefix } _ ${ method . id } ` ,
title : method . description ,
callback : ( paymentData , orderId , orderPaymentDetails ) =>
return loader . order ( paymentData , orderId , orderPaymentDetails ) ;
} ) ) ,
} catch ( error ) {
return [ ] ;
}
} ,
} ) ;
} ,
} ) ;
While payment methods provided by Front-Commerce don't need any additional
information, you may require some, for example, if you want to let users enter a
comment related to the payment method/checkout.
1 You should register a new component in getAdditionalDataComponent.js by
following this template:
my-extension/theme/modules/Checkout/Payment/AdditionalPaymentInformation/getAdditionalDataComponent.js
import MyCustomComponent from "theme/modules/CustomComponent/CustomComponent" ;
const ComponentMap = {
"<method_code>" : MyCustomComponent ,
} ;
The Custom component referenced here should be created within your
my-extension extension, and display the additional information of your payment
method to the user.
my-extension/theme/modules/CustomComponent/CustomComponent.tsx
import SubmitPayment from "theme/modules/Checkout/Payment/SubmitPayment" ;
const CustomComponent = ( {
onAuthorize ,
value ,
gscAccepted ,
error ,
method ,
} : {
onAuthorize : ( additionalData : Record < string , string > ) => void ;
value : Record < string , string > ;
gscAccepted : boolean ;
error ? : string | null ;
method : {
code : string ;
title : string ;
} ;
} ) => {
const [ comments , setComments ] = useState ( value ?. comments ?? "" ) ;
return (
< div className = " custom-component " >
< input
onChange = { ( event ) => setComments ( event . target . value ) }
value = { comments }
type = " text "
/>
< SubmitPayment
gscAccepted = { gscAccepted }
onSubmit = { ( ) => {
onAuthorize ( gscAccepted ? { comments } : null ) ;
} }
error = { error }
/>
</ div >
) ;
} ;
export default CustomComponent ;
There are a few things to understand here:
The SubmitPayment component is a common component across all payment methods
to ensure consistency in how methods are managed.
method the current selected method.
value contains initial additional data (usually it is always null).
onAuthorize should be called with the additional data on <SubmitPayment>'s
on submit method.
gscAccepted whether or not the user have accepted the general sales
conditions (checkbox above the "Place my order" button). Just forward this to
the <SubmitPayment> component.
error an error object if there is an error. Just forward this to the
<SubmitPayment> component.
In this example we're only setting a comments as additional information, but
you could as well implement a more complex form, by fetching a list for the user
to pick from GraphQL and displaying them here for example.
2 Updating an existing method Additional Data Sometimes you may need to add an extra field to the additional data of an
existing payment method. You first need to check if this payment method already
has an AdditionalDataComponent registered.
3 Adding a field to all payment methods In case you require adding a field to all existing payment methods, you should
enhance the AdditionalDataComponent by following this template:
my-extension/theme/modules/Checkout/Payment/AdditionalPaymentInformation/getAdditionalDataComponent.js
import CustomEnhancer from 'theme/modules/CustomModule/CustomEnhancer'
const ComponentMap = {
... ,
} ;
const getAdditionalDataComponent = ( method ) => {
const Component = ComponentMap [ method . code ] ;
return CustomEnhancer ( Component ) ;
} ;
export default getAdditionalDataComponent ;
The CustomEnhancer referenced here should be created in your own extension. It
should display the base component sent to it and add additional data fields and
override the onAuthorize method.
my-extension/theme/modules/CustomModule/CustomEnhancer.tsx
import SubmitPayment from "theme/modules/Checkout/Payment/SubmitPayment" ;
const CustomEnhancer =
( BaseComponent ) =>
( {
onAuthorize ,
value ,
gscAccepted ,
error ,
method ,
} : {
onAuthorize : ( additionalData : Record < string , string > ) => void ;
value : Record < string , string > ;
gscAccepted : boolean ;
error ? : string | null ;
method : {
code : string ;
title : string ;
} ;
} ) => {
const [ comments , setComments ] = useState ( value ?. comments ?? "" ) ;
return (
< div className = " custom-enhancer " >
< input
onChange = { ( event ) => setComments ( event . target . value ) }
value = { comments }
type = " text "
/>
{ BaseComponent ? (
< BaseComponent
onAuthorize = { ( additionalData ) => {
onAuthorize ( { ... additionalData , comments } ) ;
} }
value = { value }
gscAccepted = { gscAccepted }
error = { error }
method = { method }
/>
) : (
< SubmitPayment
gscAccepted = { gscAccepted }
onSubmit = { ( ) => {
onAuthorize ( gscAccepted ? { comments } : null ) ;
} }
error = { this . props . error }
/>
) }
</ div >
) ;
} ;
export default CustomEnhancer ;
Payment workflows specificities
Front-Commerce provides different hooks allowing you to use the payment method
of your choice. This section explains the implementation specificities of each
payment workflows.
The documentation here only explains changes to apply to the default
implementation example for each workflow.
Async Order
This workflow is recommended for payments that are fully front-end (e.g.
Payzen/Lyra, Paypal)
Server changes
Ensure the direct payment processor call does nothing in the extension's
contextEnhancer :
my-extension/runtime.ts
export default createGraphQLRuntime({
contextEnhancer: ({ loaders }) => {
// ...
loaders.Payment.registerEmbeddedPaymentMethod(
METHOD_CODE,
METHOD_TITLE,
- (paymentData, orderId, orderPaymentDetails) => {
- return loader.order(paymentData, orderId, orderPaymentDetails);
- },
+ () => {
+ throw new Error(
+ "The payment method should only be handled by IPN notifications"
+ );
+ },
null,
new MyPaymentNotificationProcessor(hipayConfig)
);
return {}; // you may export loaders here in case the payment provides custom Queries (to fetch a payment token for instance)
},
});
This implies the order method in the module’s loader is not needed for this
method (your may need to implement other methods instead to be used by the
NotificationProcessor)
my-extension/loaders/MyPaymentLoader.ts
export default class MyPaymentLoader {
- async order(paymentData, orderId, orderPaymentDetails) {
- // ...
- }
}
Theme changes
Override theme/pages/Checkout/checkoutFlowOf.js to indicate the use of the
asyncOrder workflow for the pway_awesomecheckout method code
app/theme/pages/Checkout/checkoutFlowOf.js
const checkoutFlowOf = (method) => {
...
+ if (method === "pway_awesomecheckout") return "asyncOrder";
return "directOrder";
};
export default checkoutFlowOf;
Direct Order
A direct order handles directly the payment on onAuthorize call in the
CustomComponent you created
(my-extension/theme/modules/CustomComponent/CustomComponent.js).
This onAuthorize call will provide the information to loader.order()
implemented server-side. This last method is responsible to handle the full
payment process.
Sub workflow : Direct Order with additional action
You may need to handle a second payment step with a Direct order workflow (3DS
validation, etc).
In this case, perform the following steps:
1 Reference the payment workflow Override theme/pages/Checkout/checkoutFlowOf.js to indicate the use of
directOrderWithAdditionalAction
app/theme/pages/Checkout/checkoutFlowOf.js
const checkoutFlowOf = (method) => {
...
+ if (method === "pway_awesomecheckout") return "directOrderWithAdditionalAction";
return "directOrder";
};
export default checkoutFlowOf;
2 Create an additional action component to handle the addtional payment step my-extension/theme/AdditionalAction/MyAdditionalAction.tsx
import { useIntl } from "react-intl" ;
const MyAdditionalAction = ( { onOrderPlacedArgs , originalOnOrderPlaced } ) => {
const intl = useIntl ( ) ;
const orderId = onOrderPlacedArgs [ 0 ] ;
return ( ) ;
} ;
export default MyAdditionalAction ;
3 Reference the additional action component my-extension/theme/modules/Checkout/PlaceOrder/getAdditionalActionComponent.js
import None from "theme/modules/Checkout/PlaceOrder/AdditionalAction/None" ;
import MyAdditionalAction from "theme/AdditionalAction/MyAdditionalAction" ;
const ComponentMap = { } ;
const getAdditionalActionComponent = ( paymentCode , paymentAdditionalData ) => {
if ( paymentCode === "pway_awesomecheckout" ) ) {
return MyAdditionalAction ;
}
return ComponentMap ?. [ paymentCode ] ?? None ;
} ;
export default getAdditionalActionComponent ;
Redirect Before Order
This workflow is only supported for Magento2, please
contact us if you need this workflow with
another backend server
A redirect before order workflow allows to interact with payment providers
handling the complete payment workflow on their side.
Front-Commerce will only redirect to the payment provider and ensure the order
creation afterwards.
Server changes
1 Register the payment method payment processor with registerRedirectBeforeOrderPaymentMethod module’s
contextEnhancer
Replace the extension's contextEnhancer with the following
my-extension/runtime.ts
export default createGraphQLRuntime ( {
contextEnhancer : ( { loaders , config } ) => {
const shopConfig = config . shop ;
const METHOD_CODE = "pway_awesomecheckout" ;
const METHOD_TITLE = "PWAy" ;
const loader = new MyPaymentLoader ( shopConfig , METHOD_CODE , ... ) ;
loaders . Payment . registerRedirectBeforeOrderPaymentMethod (
METHOD_CODE ,
METHOD_TITLE ,
loader ,
) ;
return { } ;
} ,
} ) ;
2 Implement the three required methods in the loader my-extension/loaders/MyPaymentLoader.ts
type PaymentCaptured = {
transactionId : string ;
success : boolean ;
cancel : boolean ;
}
export default class MyPaymentLoader {
constructor ( shopConfig , methodCode , ... ) {
...
this . callbackUrl = ` ${ shopConfig . url } /checkout/payment/ ${ methodCode } ` ;
}
mapCartPaymentToRedirect ( { paymentId , cart } ) {
return {
url : true ,
html : true ,
value : ` ...?callbackUrl= ${ this . callbackUrl } ` ,
}
}
async isReturnSuccess ( data ) {
return true | false ;
}
async capturePayment ( additionalData , orderId , paymentDetails ) {
return {
transactionId ,
success : true ,
cancel : true ,
} ;
}
}
Theme changes
No AdditionalDataComponent is required for this implementation as there is no
frontend component displayed by Front-Commerce
Using this workflow, you will need to override
theme/pages/Checkout/checkoutFlowOf.js to indicate the use of
redirectBeforeOrder:
app/theme/pages/Checkout/checkoutFlowOf.js
const checkoutFlowOf = (method) => {
...
+ if (method === "pway_awesomecheckout") return "redirectBeforeOrder";
return "directOrder";
};
export default checkoutFlowOf;
We encourage you to have a look at the payment extensions' source code from
Front-Commerce to learn about advanced patterns: