joshnuss / svelte-stripe

Everything you need to add Stripe Elements to your Svelte project
https://sveltestripe.com
MIT License
412 stars 40 forks source link

EmbeddedCheckout IntegrationError: You cannot have multiple Embedded Checkout objects. #97

Closed unruha closed 8 months ago

unruha commented 9 months ago

Describe the bug

First of all, thank you for the work put into this project, its providing a much cleaner checkout flow in SvelteKit for me as opposed to other methods.

When you load Embedded Checkout then navigate away client-side, and try to load the Embedded Checkout element again, you get this error on the client and the checkout doesn't load: svelte-stripe-error

The checkout element then loads again when you refresh the page.

Reproduction

Create a checkout session on the server side (outlined here). Then mount the embedded checkout element and pass it your client secret provided by the checkout session creation response. It should show correctly on the first load.

If you navigate elsewhere on the client side then return to the checkout page and try to mount the embedded checkout element again, you see the error outlined above and the element doesn't show.

This behavior is seen both with the same checkout client secret and if you generate a new client secret before mounting the element.

Refreshing the page causes the element to load correctly again.

<script lang="ts">
    import { loadStripe } from "@stripe/stripe-js";
    import { onMount } from "svelte";
    import { EmbeddedCheckout } from "svelte-stripe";

    export let data: PageData;

    let stripe = null;

    onMount(async () => {
        stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY);
    });
</script>

<div>
    <EmbeddedCheckout {stripe} clientSecret={data.clientSecret} >
    </EmbeddedCheckout>
</div>

Severity

serious, but I can work around it

Additional Information

No response

joshnuss commented 9 months ago

Hi @unruha,

What version of svelte-stripe are you using? An issue like that was recently fixed, so I'm wonder if you're on the latest version 1.1.3

unruha commented 9 months ago

Hi @joshnuss,

Thanks for the quick response. Yes I'm using version 1.1.3. I'm adding some additional details below.

I generate the Stripe checkout session in a form action on the previous page, then pass the session ID to the checkout page:

export const actions: Actions = {
    submit: async (event) => {
        const stripe = new Stripe(STRIPE_SECRET_KEY, {
            apiVersion: "2023-10-16"
        });

        // create a checkout session and pass the session ID to the checkout page
        const stripeSession: Stripe.Checkout.Session = await stripe.checkout.sessions.create({
            ui_mode: "embedded",
            line_items: [
                {
                    price: PRODUCT,
                    quantity: 1
                }
            ],
            mode: "subscription",
            return_url: `{BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
        });

        throw redirect(308, `/checkout?checkoutSessionId=${stripeSession.client_secret}`);
    }
}

Pass the checkout session ID into the page data on the checkout page:

import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async (event) => {
    return {
        checkoutSessionId: event.url.searchParams.get("checkoutSessionId")
    }
};

Lastly, I use the checkout session ID in the EmbeddedCheckout element:

<script lang="ts">
    import { loadStripe } from "@stripe/stripe-js";
    import { onMount } from "svelte";
    import { EmbeddedCheckout } from "svelte-stripe";

    export let data: PageData;

    let stripe = null;

    onMount(async () => {
        stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY);
    });
</script>

<div>
    <EmbeddedCheckout {stripe} clientSecret={data.checkoutSessionId} >
    </EmbeddedCheckout>
</div>

The embedded checkout displays correctly on the initial load. But if I navigate away and back to the checkout page, I see the error. I have also tried generating the checkout session in the load function of the checkout page (as seen in the example) but with the same result.

conorw commented 9 months ago

I had the same issue, seems the key is to destroy the stripe element when your component is destroyed. E.g. my embedded checkout control looks for the destroy event and then calls the destroy event of the stripe element:

``

unruha commented 9 months ago

@conorw thanks for the tip. I think the difference is that I'm trying to use the embedded checkout component while you're using the native HTML JS solution. I tried to replicate what you did here by destroying the EmbeddedCheckout element when the component is destroyed, but it still didn't work for me.

wrapper.svelte

<script lang="ts">
    import type { Stripe } from "@stripe/stripe-js";
    import { EmbeddedCheckout } from "svelte-stripe";
    import { onDestroy } from "svelte";

    export let clientSecret: string;
    export let stripe: Stripe;
    let checkoutRef: EmbeddedCheckout;

    onDestroy(() => {
        console.log("destroying component")
        checkoutRef.$destroy();
    });
</script>

<EmbeddedCheckout bind:this={checkoutRef} {stripe} clientSecret={clientSecret} >
</EmbeddedCheckout>

If I can't get this working I may just try to native solution that you're using. It definitely seems like an underlying issue with the lifecycle management of the embeddedCheckout on Stripe's side, and not within this library.

unruha commented 9 months ago

@joshnuss I ran the project locally and I see the same error. I tested the solution proposed by @conorw and it seemed to resolve the issue. If we are interested in this workaround, I have created a PR for the fix here.

joshnuss commented 8 months ago

I think this can be closed with #99.

Thanks for reporting and submitting the fix 👍🏻

rawwerks commented 8 months ago

related to this issue, @joshnuss - what would be the best practice for handling multiple <EmbeddedCheckout />?

i fixed the above error by adding the binding and onDestroy, but let's say i want the customer to be able to choose between two different products, which have 2 different client secrets.

am i re-assigning the clientsecret to a new value, but keeping one <EmbeddedCheckout />object? this seems to return the same error.

or do i have two <EmbeddedCheckout />somewhere each with their own client secret, but only one is shown?

Thanks!

rawwerks commented 8 months ago

i'm not sure this issue is entirely resolved. (i'm using 1.1.3, but don't see that release linked to here in GitHub)

if i show/hide using conditional html, the second time it shows i'm getting the error, even if i explicity destroy the first object upon hiding.

error:

Uncaught (in promise) IntegrationError: You cannot have multiple Embedded Checkout objects.

even when i do this on hide:

checkoutRef.$destroy();

and have bound:

<EmbeddedCheckout bind:this={checkoutRef} {stripe} clientSecret={ClientSecret}/>

rawwerks commented 8 months ago

after a bit more digging, it looks like npm hasn't been updated in a couple of months.

@joshnuss - is there an ETA on the next npm release, or should i try to find a workround from source?

joshnuss commented 7 months ago

Hi @rawwerks,

I've released a new version, can you try the latest and let me know if it's still an issue

gagansuie commented 6 months ago

@joshnuss

I still see the error in version 1.1.4. My implementation is here:

https://github.com/CodeCrowCorp/mage-website/blob/main/src/lib/components/Channel/Chat/DialogSponsor.svelte

joshnuss commented 6 months ago

@gagansuie I will investigate

In the meantime, try wrapping your dialog with a {#key condition} block. That will cause everything to re-mount when the condition changes.

gdrose commented 4 months ago

@joshnuss

Facing the same error here, I tried wrapping the dialog with the client_secret as the condition of the key block but It didn't solve the issue. Binding the and then destroying the component doesn't work on my side :(

I'm using the latest version of the package.

yfi commented 4 months ago

Same problem. Version 1.1.7, svelte 4.2.15, kit 2.5.7