Oren
Orenā€¢4mo ago

convex auth causes browser "leave site?" prompt on every redirect e.g. stripe or auth

3 Replies
sshader
sshaderā€¢4mo ago
Hmm I haven't seen this specifically with Convex Auth. Can you say more about your set up and how to reproduce this? In general, Convex hooks up an onbeforeunload listener to pop up this dialog if there are any mutations or actions that are in flight (https://github.com/get-convex/convex-js/blob/4e4e416c981ef8936ebe45bdf727d03984b7d2de/src/browser/sync/client.ts#L249) so the user doesn't end up losing data. Is it possible that you're kicking off a mutation / action immediately before trying to leave the page? For debugging, I'd try setting unsavedChangesWarning: false when instantiating your Convex client (only for debugging, because it's fairly easy for end users to experience data loss without it) or set verbose: true so you can see some logs when mutations / actions get sent.
GitHub
convex-js/src/browser/sync/client.ts at 4e4e416c981ef8936ebe45bdf72...
TypeScript/JavaScript client library for Convex. Contribute to get-convex/convex-js development by creating an account on GitHub.
Oren
OrenOPā€¢4mo ago
hmm the url comes from convex mutation but I would think as I have the url and it triggers redirect the mutation would be complete šŸ¤” I will give the debug a try thank you this is what happens when I click the button
onClick={async () => {
setPriceIdLoading(price.id);

if (token) {
try {
const { data: url } = await createCheckoutSession({
priceId: annual ? price.yearlyId! : price.id!,
});

if (!url) {
return;
} else {
window.location.assign(url);
}
} catch (error) {
console.error(error);
toast.error("Error creating checkout session");
}
} else {
router.push(routes.app.settings);
}
}}
onClick={async () => {
setPriceIdLoading(price.id);

if (token) {
try {
const { data: url } = await createCheckoutSession({
priceId: annual ? price.yearlyId! : price.id!,
});

if (!url) {
return;
} else {
window.location.assign(url);
}
} catch (error) {
console.error(error);
toast.error("Error creating checkout session");
}
} else {
router.push(routes.app.settings);
}
}}
and this is the createCheckoutSession action
export const createCheckoutSession = action({
args: { priceId: v.string() },
handler: async (ctx, { priceId }) => {
const stripe = new Stripe(process.env.STRIPE_KEY!, {
apiVersion: "2024-06-20",
});

const { data: user } = await ctx.runQuery(api.users.getCurrentUser);

let customer = user?.stripe_customer_id ?? null;

if (!user) {
return { data: null, error: "Not authenticated" };
}

if (!user.stripe_customer_id) {
... some logic
}

if (!customer) {
return { data: null, error: "No customer found" };
}

const session = (await stripe.checkout.sessions.create({
... some logic
})) as Stripe.Checkout.Session;

return { data: session.url };
},
});
export const createCheckoutSession = action({
args: { priceId: v.string() },
handler: async (ctx, { priceId }) => {
const stripe = new Stripe(process.env.STRIPE_KEY!, {
apiVersion: "2024-06-20",
});

const { data: user } = await ctx.runQuery(api.users.getCurrentUser);

let customer = user?.stripe_customer_id ?? null;

if (!user) {
return { data: null, error: "Not authenticated" };
}

if (!user.stripe_customer_id) {
... some logic
}

if (!customer) {
return { data: null, error: "No customer found" };
}

const session = (await stripe.checkout.sessions.create({
... some logic
})) as Stripe.Checkout.Session;

return { data: session.url };
},
});
this also happens when I click on oauth button
"use client";

import { useEffect } from "react";
import { useAuthActions } from "@convex-dev/auth/react";

import { routes } from "@/lib/utils";
import { Button } from "@/components/ui/button";

interface OauthButtonProps {
provider: "google";
icon: React.ReactNode;
text: string;
isLoading: boolean;
isDisabled: boolean;
setLoading: (state: boolean) => void;
}

export const OauthButton = ({
provider,
icon,
isDisabled,
setLoading,
isLoading,
text,
}: OauthButtonProps) => {
const { signIn } = useAuthActions();

useEffect(() => {
return () => setLoading?.(false);
}, [setLoading]);

return (
<Button
variant="outline"
isLoading={isLoading}
disabled={isDisabled}
onClick={async (event) => {
event.preventDefault();
event.stopPropagation();
setLoading(true);

await signIn(provider, {
redirectTo: `${window.location.origin}/${routes.app.imagine}`,
});
}}
>
<div className="flex items-center pr-3">{icon}</div>
<span className="">{text}</span>
</Button>
);
};
"use client";

import { useEffect } from "react";
import { useAuthActions } from "@convex-dev/auth/react";

import { routes } from "@/lib/utils";
import { Button } from "@/components/ui/button";

interface OauthButtonProps {
provider: "google";
icon: React.ReactNode;
text: string;
isLoading: boolean;
isDisabled: boolean;
setLoading: (state: boolean) => void;
}

export const OauthButton = ({
provider,
icon,
isDisabled,
setLoading,
isLoading,
text,
}: OauthButtonProps) => {
const { signIn } = useAuthActions();

useEffect(() => {
return () => setLoading?.(false);
}, [setLoading]);

return (
<Button
variant="outline"
isLoading={isLoading}
disabled={isDisabled}
onClick={async (event) => {
event.preventDefault();
event.stopPropagation();
setLoading(true);

await signIn(provider, {
redirectTo: `${window.location.origin}/${routes.app.imagine}`,
});
}}
>
<div className="flex items-center pr-3">{icon}</div>
<span className="">{text}</span>
</Button>
);
};
it has something to do with middleware route matcher when I use matcher as only this it causes problem (recommended from nextjs docs)
"/((?!_next/static|_next/image|api|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"
"/((?!_next/static|_next/image|api|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"
If I use it like this including convex auth recommended matcher the problem goes away
"/((?!_next/static|_next/image|api|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
"/",
"/(api|trpc)(.*)",
"/((?!_next/static|_next/image|api|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
"/",
"/(api|trpc)(.*)",
šŸ¤” gotta have it matching inclusive of
/api
/api
sshader
sshaderā€¢4mo ago
Could you try updating Convex Auth (0.0.64)? One thing I noticed was that we wouldn't clear the beforeunload listeners if we hit an error when trying to fetch a new access token, which would result in that dialog always popping up.

Did you find this page helpful?