From 035ba57d660791e21c8124307355a7c8d34cb218 Mon Sep 17 00:00:00 2001 From: Stan Wohlwend Date: Tue, 4 Jun 2024 19:45:13 +0200 Subject: [PATCH] Show alert when there's an error in UI components --- .../new-project/page-client.tsx | 6 +++--- .../src/components/form-dialog.tsx | 6 +++--- .../stack-server/src/components/settings.tsx | 8 ++++---- .../stack-server/src/components/ui/button.tsx | 4 ++-- .../src/components/ui/error-page.tsx | 2 +- .../stack-server/src/components/ui/switch.tsx | 6 +++--- packages/stack-shared/src/utils/promises.tsx | 20 +++++++++++++++++-- .../src/components/credential-sign-in.tsx | 4 ++-- .../src/components/credential-sign-up.tsx | 4 ++-- .../stack/src/components/forgot-password.tsx | 4 ++-- .../src/components/magic-link-sign-in.tsx | 4 ++-- .../src/components/password-reset-inner.tsx | 4 ++-- packages/stack/src/components/user-button.tsx | 10 +++++----- 13 files changed, 49 insertions(+), 33 deletions(-) diff --git a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx b/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx index 7db766e99..4eaf3364d 100644 --- a/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx +++ b/packages/stack-server/src/app/(main)/(protected)/(outside-dashbaord)/new-project/page-client.tsx @@ -5,7 +5,7 @@ import { Separator } from "@/components/ui/separator"; import { yupResolver } from "@hookform/resolvers/yup"; import { Form } from "@/components/ui/form"; import { InputField, SwitchListField } from "@/components/form-fields"; -import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronously, runAsynchronouslyWithAlert, wait } from "@stackframe/stack-shared/dist/utils/promises"; import { useRouter } from "@/components/router"; import { useState } from "react"; import { Button } from "@/components/ui/button"; @@ -79,7 +79,7 @@ export default function PageClient () {
- runAsynchronously(form.handleSubmit(onSubmit)(e))} className="space-y-4"> + runAsynchronouslyWithAlert(form.handleSubmit(onSubmit)(e))} className="space-y-4"> @@ -127,4 +127,4 @@ export default function PageClient () { ); -} \ No newline at end of file +} diff --git a/packages/stack-server/src/components/form-dialog.tsx b/packages/stack-server/src/components/form-dialog.tsx index 0538b4f01..64d6e431a 100644 --- a/packages/stack-server/src/components/form-dialog.tsx +++ b/packages/stack-server/src/components/form-dialog.tsx @@ -2,7 +2,7 @@ import * as yup from "yup"; import { useEffect, useId, useState } from "react"; import { ActionDialog, ActionDialogProps } from "@/components/action-dialog"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { yupResolver } from "@hookform/resolvers/yup"; import { FieldValues, useForm } from "react-hook-form"; import { Form } from "@/components/ui/form"; @@ -90,7 +90,7 @@ export function FormDialog( {...props} open={props.open ?? openState} onOpenChange={(open) => { if(open) setOpenState(true); props.onOpenChange?.(open); }} - onClose={() => { form.reset(); setOpenState(false); runAsynchronously(props.onClose?.()); }} + onClose={() => { form.reset(); setOpenState(false); runAsynchronouslyWithAlert(props.onClose?.()); }} okButton={{ onClick: async () => "prevent-close", ...(typeof props.okButton == "boolean" ? {} : props.okButton), @@ -103,7 +103,7 @@ export function FormDialog( }} > - runAsynchronously(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}> + runAsynchronouslyWithAlert(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}> {props.render(form)} diff --git a/packages/stack-server/src/components/settings.tsx b/packages/stack-server/src/components/settings.tsx index 031f9d2bf..419303322 100644 --- a/packages/stack-server/src/components/settings.tsx +++ b/packages/stack-server/src/components/settings.tsx @@ -12,7 +12,7 @@ import { Button } from "./ui/button"; import React, { useEffect, useId, useRef, useState } from "react"; import { Label } from "./ui/label"; import { DelayedInput, Input } from "./ui/input"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronously, runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { Accordion } from "@radix-ui/react-accordion"; import { AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"; import { FieldValues, useForm } from "react-hook-form"; @@ -114,7 +114,7 @@ export function SettingInput(props: { runAsynchronously(props.onChange?.(e.target.value))} + onChange={(e) => runAsynchronouslyWithAlert(props.onChange?.(e.target.value))} /> {props.actions} @@ -177,10 +177,10 @@ export function FormSettingCard( }>
- runAsynchronously(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}> + runAsynchronouslyWithAlert(form.handleSubmit(onSubmit)(e))} className="space-y-4" id={formId}> {props.render(form)}
); -} \ No newline at end of file +} diff --git a/packages/stack-server/src/components/ui/button.tsx b/packages/stack-server/src/components/ui/button.tsx index b590b9cf9..b74b7b404 100644 --- a/packages/stack-server/src/components/ui/button.tsx +++ b/packages/stack-server/src/components/ui/button.tsx @@ -3,7 +3,7 @@ import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { Spinner } from "./spinner"; import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback"; @@ -75,7 +75,7 @@ const Button = React.forwardRef( {...props} ref={ref} disabled={props.disabled || loading} - onClick={(e) => runAsynchronously(handleClick(e))} + onClick={(e) => runAsynchronouslyWithAlert(handleClick(e))} > {loading && } {children} diff --git a/packages/stack-server/src/components/ui/error-page.tsx b/packages/stack-server/src/components/ui/error-page.tsx index 84f39a225..58ed2b60a 100644 --- a/packages/stack-server/src/components/ui/error-page.tsx +++ b/packages/stack-server/src/components/ui/error-page.tsx @@ -28,7 +28,7 @@ export default function ErrorPage(props: { {props.description} - diff --git a/packages/stack-server/src/components/ui/switch.tsx b/packages/stack-server/src/components/ui/switch.tsx index 8be63e5a5..acedd0d0a 100644 --- a/packages/stack-server/src/components/ui/switch.tsx +++ b/packages/stack-server/src/components/ui/switch.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import * as SwitchPrimitives from "@radix-ui/react-switch"; import { cn } from "@/lib/utils"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback"; import { Spinner } from "./spinner"; @@ -55,8 +55,8 @@ const Switch = React.forwardRef< runAsynchronously(handleClick(e))} - onCheckedChange={(checked) => runAsynchronously(handleCheckedChange(checked))} + onClick={(e) => runAsynchronouslyWithAlert(handleClick(e))} + onCheckedChange={(checked) => runAsynchronouslyWithAlert(handleCheckedChange(checked))} disabled={props.disabled || loading} style={{ visibility: loading ? "hidden" : "visible", diff --git a/packages/stack-shared/src/utils/promises.tsx b/packages/stack-shared/src/utils/promises.tsx index 03f6b260b..289e0a7c9 100644 --- a/packages/stack-shared/src/utils/promises.tsx +++ b/packages/stack-shared/src/utils/promises.tsx @@ -96,10 +96,25 @@ class ErrorDuringRunAsynchronously extends Error { } } +export function runAsynchronouslyWithAlert(...args: Parameters) { + return runAsynchronously( + args[0], + { + ...args[1], + onError: error => { + alert(`An unhandled error occurred. Please ${process.env.NODE_ENV === "development" ? `check the browser console for the full error. ${error}` : "report this to the developer."}`); + args[1]?.onError?.(error); + }, + }, + ...args.slice(2) as [], + ); +} + export function runAsynchronously( promiseOrFunc: void | Promise | (() => void | Promise) | undefined, options: { - ignoreErrors?: boolean, + noErrorLogging?: boolean, + onError?: (error: Error) => void, } = {}, ): void { if (typeof promiseOrFunc === "function") { @@ -116,7 +131,8 @@ export function runAsynchronously( cause: error, } ); - if (!options.ignoreErrors) { + options.onError?.(newError); + if (!options.noErrorLogging) { captureError("runAsynchronously", newError); } }); diff --git a/packages/stack/src/components/credential-sign-in.tsx b/packages/stack/src/components/credential-sign-in.tsx index f0a685814..19ab6a820 100644 --- a/packages/stack/src/components/credential-sign-in.tsx +++ b/packages/stack/src/components/credential-sign-in.tsx @@ -7,7 +7,7 @@ import FormWarningText from "./form-warning"; import PasswordField from "./password-field"; import { useStackApp } from ".."; import { Button, Input, Label, Link } from "../components-core"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; const schema = yup.object().shape({ email: yup.string().email('Please enter a valid email').required('Please enter your email'), @@ -30,7 +30,7 @@ export default function CredentialSignIn() { return (
runAsynchronously(handleSubmit(onSubmit)(e))} + onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))} noValidate > diff --git a/packages/stack/src/components/credential-sign-up.tsx b/packages/stack/src/components/credential-sign-up.tsx index e401a4601..bae8d236e 100644 --- a/packages/stack/src/components/credential-sign-up.tsx +++ b/packages/stack/src/components/credential-sign-up.tsx @@ -7,7 +7,7 @@ import PasswordField from "./password-field"; import FormWarningText from "./form-warning"; import { useStackApp } from ".."; import { Label, Input, Button } from "../components-core"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import { getPasswordError } from "@stackframe/stack-shared/dist/helpers/password"; const schema = yup.object().shape({ @@ -41,7 +41,7 @@ export default function CredentialSignUp() { return ( runAsynchronously(handleSubmit(onSubmit)(e))} + onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))} noValidate > diff --git a/packages/stack/src/components/forgot-password.tsx b/packages/stack/src/components/forgot-password.tsx index b94af939e..b59089fec 100644 --- a/packages/stack/src/components/forgot-password.tsx +++ b/packages/stack/src/components/forgot-password.tsx @@ -6,7 +6,7 @@ import * as yup from "yup"; import FormWarningText from "./form-warning"; import { useStackApp } from ".."; import { Button, Input, Label } from "../components-core"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; const schema = yup.object().shape({ email: yup.string().email('Please enter a valid email').required('Please enter your email') @@ -27,7 +27,7 @@ export default function ForgotPassword({ onSent }: { onSent?: () => void }) { return ( runAsynchronously(handleSubmit(onSubmit)(e))} + onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))} noValidate > diff --git a/packages/stack/src/components/magic-link-sign-in.tsx b/packages/stack/src/components/magic-link-sign-in.tsx index 69ad3d970..ba0fc166b 100644 --- a/packages/stack/src/components/magic-link-sign-in.tsx +++ b/packages/stack/src/components/magic-link-sign-in.tsx @@ -7,7 +7,7 @@ import * as yup from "yup"; import FormWarningText from "./form-warning"; import { useStackApp } from ".."; import { Button, Input, Label } from "../components-core"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; const schema = yup.object().shape({ email: yup.string().email('Please enter a valid email').required('Please enter your email') @@ -34,7 +34,7 @@ export default function MagicLinkSignIn() { return ( runAsynchronously(handleSubmit(onSubmit)(e))} + onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))} noValidate > diff --git a/packages/stack/src/components/password-reset-inner.tsx b/packages/stack/src/components/password-reset-inner.tsx index d2ebafcff..64686388f 100644 --- a/packages/stack/src/components/password-reset-inner.tsx +++ b/packages/stack/src/components/password-reset-inner.tsx @@ -12,7 +12,7 @@ import RedirectMessageCard from "./redirect-message-card"; import MessageCard from "./message-card"; import MaybeFullPage from "./maybe-full-page"; import { Button, Label, Text } from "../components-core"; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; const schema = yup.object().shape({ password: yup.string().required('Please enter your password').test({ @@ -71,7 +71,7 @@ export default function PasswordResetInner( runAsynchronously(handleSubmit(onSubmit)(e))} + onSubmit={e => runAsynchronouslyWithAlert(handleSubmit(onSubmit)(e))} noValidate > diff --git a/packages/stack/src/components/user-button.tsx b/packages/stack/src/components/user-button.tsx index 80caf8564..b3cf0a935 100644 --- a/packages/stack/src/components/user-button.tsx +++ b/packages/stack/src/components/user-button.tsx @@ -13,7 +13,7 @@ import { Skeleton, CurrentUser, } from ".."; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; import UserAvatar from "./user-avatar"; import { useRouter } from "next/navigation"; import { typedEntries, typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects"; @@ -40,7 +40,7 @@ const icons = typedFromEntries(typedEntries({ function Item(props: { text: string, icon: React.ReactNode, onClick: () => void | Promise }) { return ( runAsynchronously(props.onClick)} + onClick={() => runAsynchronouslyWithAlert(props.onClick)} style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }} > {props.icon} @@ -116,17 +116,17 @@ function UserButtonInnerInner(props: UserButtonProps & { user: CurrentUser | nul {user && runAsynchronously(router.push(app.urls.accountSettings))} + onClick={() => router.push(app.urls.accountSettings)} icon={icons.CircleUser} />} {!user && runAsynchronously(router.push(app.urls.signIn))} + onClick={() => router.push(app.urls.signIn)} icon={icons.LogIn} />} {!user && runAsynchronously(router.push(app.urls.signUp))} + onClick={() => router.push(app.urls.signUp)} icon={icons.UserPlus} />} {user && props.extraItems && props.extraItems.map((item, index) => (