Next.js Discord

Discord Forum

Issue with Nextjs and Stripe

Answered
Berger Picard posted this in #help-forum
Open in Discord
Avatar
Berger PicardOP
Sorry for asking for this in here but Stripe doesn't offer any community and I think the bug is somehow related to Nextjs and maybe Shadcn. Please bear with me.

I'm trying to create a add payment method form by using Stripe elements and Shadcn's dialog but for unknown reasons the following hooks:
- useElements
- useStripe
returns null and simply doesn't work when trying to submit the form. And also, when I modify any part of the code that forces a hot reload it magically starts working again.

Here's what I got so far by debugging:
- loadStripe resolves correctly (I did a console log by checking it)
- I tried to upgrade to Nextjs 15 and still the issue persists (nextjs 15 doesnt offer any cache by default I guess). I'm on Nextjs 14.2.13
- dynamic((), { ssr: false }) doesn't solve the issue
- I tried adding a useEffect to call the loadStripe and set it in a useState but also doesn't solve the issue
- I tried calling the loadStripe at the main component and passing it by props to the <Elements> but still doesn't solve the issue
- I've also seen this StackOverflow [thread](https://stackoverflow.com/questions/77615172/stripe-usestripe-and-useelements-both-are-null-and-elements-do-not-render) but the answer is too vague

+
Answered by Berger Picard
found it, the useCallback didnt contain the stripe and elements as a dependency
View full answer

6 Replies

Avatar
Berger PicardOP
shadcn's dialog:
'use client'

// redacted imports to improve clarity

export type AddPaymentMethodProps = {
  open: boolean
  onClose: () => void
}

export function AddPaymentMethod({ open, onClose }: AddPaymentMethodProps) {
  const [ready, setReady] = useState(false)
  const { mutateAsync: createOrganizationBillingCardIntent } =
    useCreateOrganizationBillingCardIntent()
  const { mutateAsync: confirmCreateOrganizationBillingCardIntent } =
    useConfirmCreateOrganizationBillingCardIntent()
  const { toast } = useToast()

  const {
    mutateAsync: submit,
    error,
    isPending: isSubmitting,
  } = useMutation({
    mutationFn: async ({
      confirm,
    }: Parameters<FillPaymentFormProps['handleSubmit']>[0]) => {
      const { clientSecret, intentId } =
        await createOrganizationBillingCardIntent()

      await confirm({ clientSecret })

      await confirmCreateOrganizationBillingCardIntent({
        intentId,
      })

      toast({
        title: 'Payment method added',
        description: 'Your payment method has been added successfully',
      })

      onClose()
    },
  })

  const handleSubmit = useCallback(
    async ({
      confirm,
    }: Parameters<FillPaymentFormProps['handleSubmit']>[0]) => {
      await submit({ confirm })
    },
    [submit],
  )

  return (
    <Dialog open={open} onOpenChange={onClose}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Add payment method</DialogTitle>
          <DialogDescription>
            Add a new payment method to your account.
          </DialogDescription>
        </DialogHeader>
        <FillPayment mode="setup" currency="usd"> 
          <FillPaymentForm
            className="flex flex-col"
            onReady={() => setReady(true)}
            handleSubmit={handleSubmit}
          >
            {error && (
              <p className="text-center text-red-500">{error.message}</p>
            )}
            <Button
              disabled={!ready || isSubmitting}
              type="submit"
              className="w-fit self-end"
            >
              Save Payment Method
            </Button>
          </FillPaymentForm>
        </FillPayment>
      </DialogContent>
    </Dialog>
  )
}
fill payment, the actual form:
'use client'

type SetupMode = {
  mode: 'setup'
}

type PaymentMode = {
  mode: 'payment'
  amount: number
}

type BaseProps = {
  children: React.ReactNode
  currency: string
}

type FillPaymentProps = BaseProps & (SetupMode | PaymentMode)

export function FillPayment(props: FillPaymentProps) {
  const { theme } = useTheme()

  const appearance =
    theme === 'dark'
      ? {
          variables: {
            colorBackground: '#000',
            colorText: '#fff',
          },
        }
      : undefined

  return (
    <Elements
      options={{
        mode: props.mode,
        amount: props.mode === 'payment' ? props.amount : undefined,
        currency: props.currency,
        paymentMethodTypes: ['card'],
        appearance,
      }}
      stripe={stripePromise}
    >
      {props.children}
    </Elements>
  )
}

export type FillPaymentFormProps = {
  children: React.ReactNode
  className?: string
  onReady: () => void
  handleSubmit: ({
    confirm,
  }: {
    confirm: ({
      clientSecret,
    }: {
      clientSecret: string
    }) => Promise<SetupIntent>
  }) => void
}

export function FillPaymentForm({
  handleSubmit,
  onReady,
  className,
  children,
}: FillPaymentFormProps) {
  const stripe = useStripe()
  const elements = useElements()

  const submit = useCallback(
    async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault()
      // stripe and elements will be null when submitting
      if (!stripe || !elements) return

      await elements.submit()

      handleSubmit({
        confirm: async ({ clientSecret }) => {
          try {
            const { error, setupIntent } = await stripe.confirmSetup({
              clientSecret,
              elements,
              redirect: 'if_required',
            })

            if (error) throw error

            return setupIntent
          } catch (error) {
            if (
              typeof error === 'object' &&
              error &&
              'message' in error &&
              typeof error.message === 'string'
            )
              throw new Error(error.message)

            throw new Error(
              'Unable to confirm your payment method. Try again later or another payment method.',
            )
          }
        },
      })
    },
    [handleSubmit],
  )

  return (
    <form onSubmit={submit} className={cn('space-y-4', className)}>
      <PaymentElement onReady={onReady} />
      {children}
    </form>
  )
}
Avatar
Berger PicardOP
demo
Image
Avatar
@Berger Picard you might want to head over directly to stripe's server: https://discord.gg/stripe
Avatar
Berger PicardOP
thanks, didnt even know that existed
Avatar
Berger PicardOP
found it, the useCallback didnt contain the stripe and elements as a dependency
Answer