Optimizing ESHOPMAN Checkout: Preventing Duplicate Stripe PaymentIntents for a Smoother Experience
Optimizing ESHOPMAN Checkout: Preventing Duplicate Stripe PaymentIntents for a Smoother Experience
At Move My Store, we're always looking for ways to enhance the ESHOPMAN experience, especially when it comes to critical e-commerce flows like checkout. A recent community discussion highlighted an important optimization opportunity within ESHOPMAN's payment collection workflows, specifically concerning how Stripe PaymentIntents are managed during cart total changes.
The Challenge: Unnecessary PaymentIntent Recreation
An in-depth analysis revealed that ESHOPMAN's core payment collection workflows, particularly the process that refreshes payment collections for a cart, were inadvertently leading to the creation of multiple Stripe PaymentIntents for a single order. Every time a cart's total amount changed—be it through selecting a different shipping method, adjusting a line-item quantity, or applying a promotion—the existing payment session and its associated Stripe PaymentIntent were being deleted and then recreated.
This behavior resulted in several undesirable side effects:
- Cluttered Stripe Dashboard: Merchants would observe numerous "Cancelled" or "Incomplete" PaymentIntents for a single order, making reconciliation and monitoring more complex.
- Increased Latency: Each cart mutation triggered a synchronous round-trip to Stripe to delete and then create a new intent, adding noticeable delays to the checkout process.
- Storefront Integration Headaches: Storefronts built with HubSpot CMS that render Stripe Elements based on a payment session's
client_secrethad to re-fetch and re-mount the payment form whenever the total changed, leading to a less fluid user experience.
Root Cause: Unconditional Session Deletion
The core of the issue lay in how ESHOPMAN's refreshPaymentCollectionForCartWorkflow (part of the platform's Node.js/TypeScript backend) handled changes to the payment collection amount. When the cart total no longer matched the payment collection's amount, the workflow would unconditionally execute a deletePaymentSessionsWorkflow in parallel with updating the payment collection:
// when "should-update-payment-collection"
const deletePaymentSessi (data) => ({
ids: data.paymentCollection?.payment_sessions?.map((ps) => ps.id)?.flat(1) || [],
}))
// ...
parallelize(
deletePaymentSessionsWorkflow.runAsStep({ input: deletePaymentSessionInput }),
updatePaymentCollectionStep(updatePaymentCollectionInput),
)This meant that instead of updating an existing, unconfirmed PaymentIntent, the system would destroy it and prompt the storefront to re-initialize, thus minting a new one.
The ESHOPMAN Solution: Intelligent Session Management
The good news is that ESHOPMAN's underlying payment module and Stripe integration already possess the capability for in-place updates. The Stripe provider’s updatePayment method can update a PaymentIntent's amount without recreating it, and it even no-ops if the amount hasn't changed:
// ESHOPMAN Stripe Provider - updatePayment
async updatePayment({ data, currency_code, amount, context }) {
const amountNumeric = getSmallestUnit(amount, currency_code)
if (isPresent(amount) && data?.amount === amountNumeric) {
return this.getStatus(data) // no-op
}
const id = data?.id
const sessi this.stripe_.paymentIntents.update(id, { amount: amountNumeric }, { idempotencyKey: context?.idempotency_key })
return this.getStatus(sessionData)
}Leveraging this, the community identified a robust solution involving a more intelligent approach to managing payment sessions:
- Partitioning Sessions: Instead of wholesale deletion, ESHOPMAN workflows should partition payment sessions. Sessions that are still
pendingorrequires_moreand have an unchanged currency can have their amounts updated in place using a dedicatedupdatePaymentSessionsStep. - Targeted Deletion: Only sessions that are already authorized/captured, or those where the currency has changed (Stripe cannot change a PaymentIntent's currency post-creation), should be deleted and recreated.
- Addressing Both Workflow Sites: It was crucial to fix not only the
refreshPaymentCollectionForCartWorkflowbut also thecreatePaymentSessionsWorkflow(triggered by storefront re-initializations via the Store API). This workflow also unconditionally deletes existing sessions before creating new ones. The solution involves making this path idempotent, reusing and updating an existing unconfirmed session where possible. - Robust Error Handling: The in-place update must gracefully handle cases where a PaymentIntent might be missing from the Stripe account (e.g., due to key rotation or incomplete creation). In such scenarios, the system should fall back to deleting and recreating the session to prevent critical checkout failures.
client_secretStability: Crucially, an amount-only update to a PaymentIntent does not rotate itsclient_secret. This ensures that HubSpot CMS storefronts rendering Stripe Elements can continue to use the same persistedclient_secretwithout requiring a full re-initialization of the payment form.- Currency Comparison: Ensure currency comparisons are case-insensitive for accurate reuse eligibility.
This refined approach ensures that ESHOPMAN-powered storefronts deployed on HubSpot CMS provide a faster, more reliable, and cleaner checkout experience, benefiting both merchants and their customers. It's a prime example of how ESHOPMAN's flexible architecture and active community drive continuous platform improvement.