fuentesloic / nuxt-stripe

MIT License
89 stars 8 forks source link

Feature Request: Automatically Inject Stripe.js on Every Page #35

Open rktmatt opened 2 months ago

rktmatt commented 2 months ago

Summary

I recently encountered issues using the nuxt-stripe package on a slow network. After some debugging, I discovered that the useClientStripe() composable returns a promise to load Stripe.js v3.

Problem

The promise when awaited doesn't guarantee that Stripe is loaded. Example :

const stripe = (await useClientStripe()) as Stripe; // otherwise types aren't working on my machine
const elements = stripe.elements(); // ts error: elements is not a function

To work around this, I had to watch for the Stripe object and use it once it had loaded. Edit: this didn't work for submitting form. So I used @stripe/stripe-js as a plugin and stopped using this package

Stripe's Recommendation

Stripe's documentation advises that Stripe.js should be loaded on every page to enhance fraud detection:

Include the Stripe.js script on each page of your site. It should always be loaded directly from https://js.stripe.com, rather than included in a bundle or hosted yourself.

To best leverage Stripe’s advanced fraud functionality, include this script on every page, not just the checkout page. This allows Stripe to detect suspicious behavior that may be indicative of fraud as customers browse your website.

Proposal

Would it be possible to add a configuration option to the nuxt-stripe package that allows Stripe.js to be injected on every page and available ? This would align with Stripe's best practices and improve user experience, especially on slower networks.

Thank you for considering this enhancement!

flozero commented 2 months ago

hey @rktmatt The thing is we do follow stripe recommandation. They do recommand to use async load for it. Thats what we do by using their js module as its them who handle it.

Screenshot 2024-06-18 at 11 07 07 AM

I would say that you would have no choice than waiting it to be available and handle use case for slow network. I general having a loading state would be a better alternative as you would not block the user interaction and load page for the end user

rktmatt commented 2 months ago

I have no problem with async loading of the script. My issue is that the module doesn't load the script at all when a page isn't using it.

The other issue is that the await doesn't await Stripe, but await the start of loading Stripe. Which makes the module not working when doing a simple : useClientStripe()

Also having no types out of the box is not very convenient.

Or am I using it wrong ?

flozero commented 2 months ago

We normally do have typing yes probably something wrong on your side. I would need a repo to see why you have an issue.

You right. In the doc we do have check on the if stripe

https://github.com/fuentesloic/nuxt-stripe?tab=readme-ov-file#example-1

If you want to update this behavior don't hesitate to update the code. But this should be enough for you I think to work with.

rktmatt commented 2 months ago

Thanks for your support. Here is a reproduction link : https://github.com/rktmatt/stripe-nuxt-repro

All in app.vue

flozero commented 2 months ago

Thanks I have a lot of works but try to look later this weekend

alanaasmaa commented 3 weeks ago

I modified the files for my own use and came up something like this. Also using latest versions of stripe and stripe-js.

Not sure if best approach but works for me.

useClientStripe.ts

import { loadStripe } from '@stripe/stripe-js'
import type { Stripe } from '@stripe/stripe-js'
import { onMounted, watch, computed } from 'vue'

export default function useClientStripe( onReadyCallback?: ( stripe: Stripe | null ) => void | Promise<void> ) {
  const stripe = useState<Stripe | null>( 'stripe-client', () => null )
  const ready = useState( 'stripe-client-ready', () => false )
  const key = useRuntimeConfig().public.stripePublicKey

  async function _loadStripe() {
    if ( stripe.value ) {
      return stripe.value
    }

    if ( !key ) console.warn( 'no key given for Stripe client service' )

    const _stripe = await loadStripe( key )
    stripe.value = _stripe
    ready.value = true

    return _stripe
  }

  onMounted( async () => {
    if ( !ready.value ) {
      await _loadStripe()
    }
  } )

  watch( ready, async ( newReady ) => {
    if ( newReady && onReadyCallback ) {
      await onReadyCallback( stripe.value )
    }
  } )

  return {
    stripe,
    ready: computed( () => ready.value ),
  }
}

Can be used for example like this

const initCheckout = async ( stripe: Stripe | null ) => {
  if ( !stripe ) {
    throw new Error( 'Stripe is not available' )
  }
  if ( !checkoutSession.value ) {
    throw new Error( 'Checkout session is not available' )
  }
  const fetchClientSecret = async () => {
    return checkoutSession.value?.clientSecret || ''
  }
  const checkout = await stripe.initEmbeddedCheckout( {
    fetchClientSecret,
  } )

  checkout.mount( '#checkout' )
}

useClientStripe( initCheckout )

OR

useClientStripe( async ( stripe ) => {
  if ( !stripe ) {
    throw new Error( 'Stripe is not available' )
  }
  if ( !checkoutSession.value ) { 
    throw new Error( 'Checkout session is not available' )
  }
  const fetchClientSecret = async () => {
    return checkoutSession.value?.clientSecret || ''
  }
  const checkout = await stripe.initEmbeddedCheckout( {
    fetchClientSecret,
  } )

  checkout.mount( '#checkout' )
} )

OR

const { stripe, ready } = useClientStripe()

watch(ready, (newReady) => {
  if (newReady) {
    // Stripe client is ready, you can use it now
    console.log('Stripe client is ready:', stripe.value)
  }
})

onMounted(() => {
  if (ready.value) {
    // Stripe client was already ready at mount
    console.log('Stripe client is ready:', stripe.value)
  }
})