Skip to main content
Sessions have a limited lifetime and will eventually expire. How you handle session expiration significantly impacts application performance and user experience. Below are the different approaches, organized from best to worst practice.

Recommended: Stateful with Proactive Validation

This is the best approach for production applications. Store the session (in localStorage, cookies, or memory) and proactively validate its expiration before displaying the checkout form.

Implementation:

  1. When creating a session on the server, store both the session token and its expiration time
  2. Before displaying the checkout form, check if the stored session is still valid
  3. If the session has expired, create a new session first
  4. Only then display the checkout form with the valid session

Example:

'use client'

import { useEffect, useState } from 'react'
import { PayNextCheckout } from '@paynext/sdk'
import '@paynext/sdk/styles'

interface StoredSession {
  id: string
  expiryDate: string
}

const CheckoutForm: React.FC = () => {
  const [clientToken, setClientToken] = useState<string>('')

  useEffect(() => {
    let checkout: PayNextCheckout | undefined

    const mountCheckout = async (token: string) => {
      if (!token) return

      checkout = new PayNextCheckout()

      await checkout.mount('paynext-checkout', {
        clientToken: token,
        environment: 'sandbox',
        // ... other checkout config options
      })
    }

    const validateAndMountCheckout = async () => {
      // Get stored session from localStorage
      const storedSessionData = localStorage.getItem('paynext_session')
      let storedSession: StoredSession | null = null

      if (storedSessionData) {
        try {
          storedSession = JSON.parse(storedSessionData)
        } catch (e) {
          console.error('Failed to parse stored session', e)
        }
      }

      // Validate session expiration
      if (storedSession && new Date(storedSession.expiryDate) > new Date()) {
        // Session is still valid - use it
        console.info('Using existing valid session')
        setClientToken(storedSession.id)
      } else {
        // Session expired or doesn't exist - create new one
        console.info('Session expired or missing, creating new session')
        
        try {
          const response = await fetch('/api/client-session', { method: 'POST', body: JSON.stringify(payload) })
          const data = await response.json()

          // Store new session with expiry date
          localStorage.setItem('paynext_session', JSON.stringify({
            id: data.id,
            expiryDate: data.expiry_date,
          }))

          setClientToken(data.id)
        } catch (error) {
          console.error('Failed to create session', error)
        }
      }
    }

    if (!clientToken) {
      validateAndMountCheckout()
    } else {
      mountCheckout(clientToken)
    }

    return () => {
      checkout?.unmount()
    }
  }, [clientToken])

  return <div id='paynext-checkout' />
}

export default CheckoutForm

Benefits:

  • Fast render time - form displays immediately with a valid session
  • Best user experience - no errors or delays
  • Optimal performance - reuses valid sessions, creates new ones only when needed

Optional: Reactive Error Handling

Use this only if you cannot implement proactive validation. Do not validate session lifetime upfront. Instead, handle expiration errors reactively when they occur.

Implementation:

  1. Display the checkout form with the existing session
  2. When the session expires, the form will return an error
  3. Catch this error, create a new session, and re-render the form

Example:

'use client'

import { useEffect, useState } from 'react'
import { PayNextCheckout } from '@paynext/sdk'
import '@paynext/sdk/styles'

const CheckoutForm: React.FC = () => {
  const [clientToken, setClientToken] = useState<string>('')
  const [sessionError, setSessionError] = useState<boolean>(false)

  useEffect(() => {
    let checkout: PayNextCheckout | undefined

    const mountCheckout = async (token: string) => {
      if (!token) return
      checkout = new PayNextCheckout()

      await checkout.mount('paynext-checkout', {
        clientToken: token,
        environment: 'sandbox',
        onCheckoutLoaded: (result) => {
          if (!result.success) {
            // Check if error is due to expired session
            if (result.error?.status === 'SESSION_EXPIRED') {
              console.warn('Session expired, creating new one')
              setSessionError(true)
              return
            }
            console.error('Checkout loading failed:', result.error?.status_reason?.message)
            return
          }
          console.info('Checkout loaded successfully')
          setSessionError(false)
        },
        onCheckoutFail: (error) => {
          // Handle session expiration during payment
          if (error.status === 'SESSION_EXPIRED') {
            console.warn('Session expired during payment')
            setSessionError(true)
            return
          }
          console.error('Payment failed:', error.status, error.status_reason?.message)
        },
        // ... other checkout config options
      })
    }

    const createNewSession = async () => {
      try {
        const response = await fetch('/api/client-session', { method: 'POST', body: JSON.stringify(payload) })
        const data = await response.json()

        // Optionally store session (though not validating expiry)
        localStorage.setItem('paynext_session', JSON.stringify({
          id: data.id,
          expiryDate: data.expiry_date,
        }))

        setClientToken(data.id)
      } catch (error) {
        console.error('Failed to create session', error)
      }
    }

    if (sessionError) {
      // Session expired - create new one and remount
      checkout?.unmount()
      createNewSession()
    } else if (!clientToken) {
      // Initial load - get stored session or create new one
      const storedSessionData = localStorage.getItem('paynext_session')
      if (storedSessionData) {
        try {
          const storedSession = JSON.parse(storedSessionData)
          setClientToken(storedSession.id)
        } catch (e) {
          createNewSession()
        }
      } else {
        createNewSession()
      }
    } else {
      mountCheckout(clientToken)
    }

    return () => {
      checkout?.unmount()
    }
  }, [clientToken, sessionError])

  return <div id='paynext-checkout' />
}

export default CheckoutForm

Drawbacks:

  • User sees an error before the form reloads
  • Requires additional error handling logic
  • Suboptimal user experience (error → reload flow)
While this approach works, it creates unnecessary friction for users. Use proactive validation whenever possible.

Not Recommended: Stateless (No Session Storage)

Avoid this approach in production applications. Never store the session anywhere. Always generate a new session every time you need to display the checkout form.

Implementation:

'use client'

import { useEffect, useState } from 'react'
import { PayNextCheckout } from '@paynext/sdk'
import '@paynext/sdk/styles'

const CheckoutForm: React.FC = () => {
  const [clientToken, setClientToken] = useState<string>('')

  useEffect(() => {
    let checkout: PayNextCheckout | undefined

    const mountCheckout = async (token: string) => {
      if (!token) return
      checkout = new PayNextCheckout()
      await checkout.mount('paynext-checkout', {
        clientToken: token,
        environment: 'sandbox',
        // ... other checkout config options
      })
    }

    // ❌ BAD: Always create new session - no storage, no reuse
    const createAndMountCheckout = async () => {
      try {
        // Every component mount creates a new session
        const response = await fetch('/api/client-session', { method: 'POST', body: JSON.stringify(payload) })
        const data = await response.json()

        // Not storing the session anywhere - it's lost after unmount
        setClientToken(data.id)
      } catch (error) {
        console.error('Failed to create session', error)
      }
    }

    if (!clientToken) {
      createAndMountCheckout() // Slow network request every time
    } else {
      mountCheckout(clientToken)
    }

    return () => {
      checkout?.unmount()
      // Session is lost here - next mount will create another one
    }
  }, [clientToken])

  return <div id='paynext-checkout' />
}

export default CheckoutForm

Why This is Bad:

  • Slow render time - must wait for server request before every form display
  • Increased server load - creates unnecessary sessions
  • Poor performance - network latency delays form appearance
  • Bad UX - users wait longer for the form to appear
Do not use this approach. It significantly degrades performance and user experience. Always store and reuse valid sessions.

Best Practice: Always implement stateful session management with proactive expiration validation for optimal performance and user experience.