stripe / stripe-apps

Stripe Apps lets you embed custom user experiences directly in the Stripe Dashboard and orchestrate the Stripe API.
https://stripe.com/docs/stripe-apps
MIT License
148 stars 73 forks source link

Browser crashes when Stripe dashboard prompts to confirm password when navigating to FocusView #943

Closed bensontrent closed 10 months ago

bensontrent commented 11 months ago

The browser crashes when, after a period of user inactivity in the Stripe dashboard, an attempt is made to update a Stripe object, thus presenting a "Confirm your password" dialog box in the Stripe dashboard. This happens when the request to update an invoice is called from within a Focus view in a MemoryRouter.

To Reproduce

  1. Create a FocusView within a MemoryRouter, then make any "update" call on the Stripe API, for example:
import { MemoryRouter, Routes, Route, useNavigate, useParams } from 'react-router-dom'
import Stripe from 'stripe';
import { useEffect, useCallback } from 'react';
import { Button, ContextView, FocusView } from '@stripe/ui-extension-sdk/ui';
import { ExtensionContextValue } from '@stripe/ui-extension-sdk/context';
import { STRIPE_API_KEY, createHttpClient } from "@stripe/ui-extension-sdk/utils/httpClient";

const stripeClient = new Stripe(STRIPE_API_KEY, {
    httpClient: createHttpClient(),
    apiVersion: '2023-08-16',
});

const InvoiceDetailView = ({ userContext, environment, appContext }: ExtensionContextValue) => {

    const context = {
        userContext,
        environment,
        appContext
    };

    return (
        <MemoryRouter>
            <Routes>
                <Route path='/' element={<InvoiceHomeView context={context} />} />
                <Route path='/invoice/:id' element={<BrowserCrashTest />} />
            </Routes>
        </MemoryRouter>
    )
}

const InvoiceHomeView = ({ context }: { context: ExtensionContextValue }) => {

    const navigate = useNavigate();

    const navigateToInvoice = () => {
        if (context.environment && context.environment.objectContext?.id) {
            navigate(`/invoice/${context.environment.objectContext.id}`);
        }
    };

    return (
    <ContextView title="Browser crash test"> 
    <Button type="primary" onPress={navigateToInvoice}>Test for browser crash</Button>
     </ContextView>
    )
}

const BrowserCrashTest = () => {
    const { id } = useParams<"id">();
    const navigate = useNavigate();

    const artificiallyTriggerPasswordPrompt = useCallback(async (id) => {
        if (id) {
            await stripeClient.invoices.update(id, { metadata: { preship: "" } })
        }
    }, [])

    useEffect(() => {
        if (id?.includes('in_')) {
            artificiallyTriggerPasswordPrompt(id);
        }
    }, [id]);

    return (
        <FocusView
            title="Trigger Stripe password prompt"
            shown={true}
            onClose={() => { navigate("/") }}>
            Triggering password prompt  (This test will only trigger an error if enough time has passed to prompt the Stripe user to confirm their password after a long period of inactivity in the Stripe dashboard)
        </FocusView>
    );
};

export default InvoiceDetailView

Screenshots

image image image

image image

Desktop

gabrielhurley-stripe commented 10 months ago

I've flagged this to the team internally and we'll investigate. Thanks for the report!

bensontrent commented 10 months ago

I was able to resolve this issue by switching to a ContextView as required by the docs. But this error as reported in the example I gave can be repeated in multiple environments.

ericfrank-stripe commented 10 months ago

Hey there, we haven't tested using Stripe Apps with React Router. Can you tell me more about your use case? Are you trying to set up navigation within your app that alters the URL? Thanks.

bensontrent commented 10 months ago

No, I'm not attempting to alter the URL. The memory router doesn't behave this way, the URL in the memory router is more of an internal route used to distribute chunks of pages so a very large app can load efficiently; without loading unnecessary components. To reproduce the error, I did leave-out one key detail: The InvoiceDetailView component in my example should be referenced in your stripe-app.json views object as:

 {
        "viewport": "stripe.dashboard.invoice.detail",
        "component": "InvoiceDetailView "
 }

You would then need to navigate to a individual invoice view in Stripe (after a period of inactivity sufficient to trigger a password prompt). At that point, it's a guaranteed browser crash (according to my tests on multiple environments).

That said, I have resolved the error by following the documentation indicating that a FocusView should never be the root component. Rather, a FocusView should always live as a child of a ContextView.

You're welcome to close this issue.

ericfrank-stripe commented 10 months ago

Great, thanks Benson.