From 3df1def41e7441c321b3a945e5a40c67afa43db1 Mon Sep 17 00:00:00 2001 From: Stan Wohlwend Date: Sun, 14 Apr 2024 08:28:40 +0200 Subject: [PATCH] Async onClick for button --- .../src/hooks/use-async-callback.tsx | 42 +++++++++++++++++++ .../src/interface/serverInterface.ts | 2 - .../stack/src/components-core-joy/button.tsx | 4 +- packages/stack/src/components-core/button.tsx | 7 +++- packages/stack/src/components-core/index.tsx | 37 ++++++++++------ .../src/components-page/account-settings.tsx | 37 ++++++++-------- .../src/components/credential-sign-in.tsx | 3 +- .../src/components/credential-sign-up.tsx | 14 ++----- .../stack/src/components/forgot-password.tsx | 7 +--- .../stack/src/components/oauth-button.tsx | 3 +- .../src/components/password-reset-inner.tsx | 3 +- packages/stack/src/components/user-button.tsx | 2 +- packages/stack/src/lib/auth.ts | 2 + .../src/providers/component-provider.tsx | 2 +- 14 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 packages/stack-shared/src/hooks/use-async-callback.tsx diff --git a/packages/stack-shared/src/hooks/use-async-callback.tsx b/packages/stack-shared/src/hooks/use-async-callback.tsx new file mode 100644 index 000000000..b85f9f8f7 --- /dev/null +++ b/packages/stack-shared/src/hooks/use-async-callback.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +export function useAsyncCallback( + callback: (...args: A) => Promise, + deps: React.DependencyList +): [cb: (...args: A) => Promise, loading: boolean, error: unknown | undefined] { + const [error, setError] = React.useState(undefined); + const [loadingCount, setLoadingCount] = React.useState(0); + + const cb = React.useCallback( + async (...args: A) => { + setLoadingCount((c) => c + 1); + try { + return await callback(...args); + } catch(e) { + setError(e); + throw e; + } finally { + setLoadingCount((c) => c - 1); + } + }, + deps, + ); + + return [cb, loadingCount > 0, error]; +} + +export function useAsyncCallbackWithLoggedError( + callback: (...args: A) => Promise, + deps: React.DependencyList +): [cb: (...args: A) => Promise, loading: boolean] { + const [newCallback, loading] = useAsyncCallback(async (...args) => { + try { + return await callback(...args); + } catch (e) { + console.error("Uncaught error in async callback", e); + throw e; + } + }, deps); + + return [newCallback, loading]; +} diff --git a/packages/stack-shared/src/interface/serverInterface.ts b/packages/stack-shared/src/interface/serverInterface.ts index 21714a7d3..30c217f62 100644 --- a/packages/stack-shared/src/interface/serverInterface.ts +++ b/packages/stack-shared/src/interface/serverInterface.ts @@ -7,8 +7,6 @@ import { ReadonlyTokenStore, } from "./clientInterface"; import { Result } from "../utils/results"; -import { AsyncCache } from "../utils/caches"; -import { runAsynchronously } from "../utils/promises"; import { ReadonlyJson } from "../utils/json"; export type ServerUserJson = UserJson & { diff --git a/packages/stack/src/components-core-joy/button.tsx b/packages/stack/src/components-core-joy/button.tsx index 61e1508c4..936549dc0 100644 --- a/packages/stack/src/components-core-joy/button.tsx +++ b/packages/stack/src/components-core-joy/button.tsx @@ -32,7 +32,7 @@ export const Button = React.forwardRef< ).toString(); }; - return {children} ; -}); \ No newline at end of file +}); diff --git a/packages/stack/src/components-core/button.tsx b/packages/stack/src/components-core/button.tsx index 08d167cc7..4631b5d7f 100644 --- a/packages/stack/src/components-core/button.tsx +++ b/packages/stack/src/components-core/button.tsx @@ -96,7 +96,8 @@ export type ButtonProps = { color?: string, size?: 'sm' | 'md' | 'lg', loading?: boolean, -} & Omit, 'size' | 'type'> + onClick?: (() => void) | (() => Promise), +} & Omit, 'size' | 'type' | 'onClick'> const StyledButton = styled.button<{ $size: 'sm' | 'md' | 'lg', @@ -180,4 +181,6 @@ const Button = React.forwardRef( Button.displayName = 'Button'; -export { Button }; \ No newline at end of file +export { + Button, +}; diff --git a/packages/stack/src/components-core/index.tsx b/packages/stack/src/components-core/index.tsx index 337807544..6b5002c92 100644 --- a/packages/stack/src/components-core/index.tsx +++ b/packages/stack/src/components-core/index.tsx @@ -3,26 +3,26 @@ import React from 'react'; import { forwardRef } from 'react'; import { Components, useComponents } from '../providers/component-provider'; -import { Button as StaticButton } from './button'; -import { Container as StaticContainer } from './container'; -import { Separator as StaticSeparator } from './separator'; -import { Input as StaticInput } from './input'; -import { Label as StaticLabel } from './label'; -import { Link as StaticLink } from './link'; -import { Text as StaticText } from './text'; -import { +import type { Button as StaticButton } from './button'; +import type { Container as StaticContainer } from './container'; +import type { Separator as StaticSeparator } from './separator'; +import type { Input as StaticInput } from './input'; +import type { Label as StaticLabel } from './label'; +import type { Link as StaticLink } from './link'; +import type { Text as StaticText } from './text'; +import type { Card as StaticCard, CardHeader as StaticCardHeader, CardContent as StaticCardContent, CardFooter as StaticCardFooter, CardDescription as StaticCardDescription, } from './card'; -import { +import type { Popover as StaticPopover, PopoverContent as StaticPopoverContent, PopoverTrigger as StaticPopoverTrigger, } from './popover'; -import { +import type { DropdownMenu as StaticDropdownMenu, DropdownMenuTrigger as StaticDropdownMenuTrigger, DropdownMenuContent as StaticDropdownMenuContent, @@ -30,23 +30,34 @@ import { DropdownMenuLabel as StaticDropdownMenuLabel, DropdownMenuSeparator as StaticDropdownMenuSeparator, } from './dropdown'; -import { +import type { Avatar as StaticAvatar, AvatarFallback as StaticAvatarFallback, AvatarImage as StaticAvatarImage, } from './avatar'; -import { +import type { Collapsible as StaticCollapsible, CollapsibleTrigger as StaticCollapsibleTrigger, CollapsibleContent as StaticCollapsibleContent, } from './collapsible'; +import { useAsyncCallbackWithLoggedError } from '@stackframe/stack-shared/dist/hooks/use-async-callback'; export const Button = forwardRef< HTMLButtonElement, React.ComponentProps >((props, ref) => { const { Button } = useComponents(); - return diff --git a/packages/stack/src/components/forgot-password.tsx b/packages/stack/src/components/forgot-password.tsx index b181fe5a2..ea6d95215 100644 --- a/packages/stack/src/components/forgot-password.tsx +++ b/packages/stack/src/components/forgot-password.tsx @@ -4,14 +4,12 @@ import { useState } from "react"; import FormWarningText from "./form-warning"; import { validateEmail } from "../utils/email"; import { useStackApp } from ".."; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; import { Button, Input, Label } from "../components-core"; export default function ForgotPassword({ onSent }: { onSent?: () => void }) { const [email, setEmail] = useState(''); const [emailError, setEmailError] = useState(''); - const [sending, setSending] = useState(false); const stackApp = useStackApp(); const onSubmit = async () => { @@ -23,9 +21,7 @@ export default function ForgotPassword({ onSent }: { onSent?: () => void }) { setEmailError('Please enter a valid email'); return; } - setSending(true); await stackApp.sendForgotPasswordEmail(email); - setSending(false); onSent?.(); }; @@ -47,8 +43,7 @@ export default function ForgotPassword({ onSent }: { onSent?: () => void }) { diff --git a/packages/stack/src/components/oauth-button.tsx b/packages/stack/src/components/oauth-button.tsx index 26dc5f2c9..94c7c65a9 100644 --- a/packages/stack/src/components/oauth-button.tsx +++ b/packages/stack/src/components/oauth-button.tsx @@ -2,7 +2,6 @@ import { FaGithub, FaFacebook, FaApple } from 'react-icons/fa'; import { useStackApp } from '..'; -import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; import { Button } from "../components-core"; import { useDesign } from "../providers/design-provider"; import Color from 'color'; @@ -115,7 +114,7 @@ export default function OAuthButton({ diff --git a/packages/stack/src/components/user-button.tsx b/packages/stack/src/components/user-button.tsx index 3616e8629..8b78a215e 100644 --- a/packages/stack/src/components/user-button.tsx +++ b/packages/stack/src/components/user-button.tsx @@ -47,4 +47,4 @@ export default function UserButton() { ); -} \ No newline at end of file +} diff --git a/packages/stack/src/lib/auth.ts b/packages/stack/src/lib/auth.ts index ec54832b9..c270db2ea 100644 --- a/packages/stack/src/lib/auth.ts +++ b/packages/stack/src/lib/auth.ts @@ -2,6 +2,7 @@ import { StackClientInterface } from "@stackframe/stack-shared"; import { saveVerifierAndState, getVerifierAndState } from "./cookie"; import { constructRedirectUrl } from "../utils/url"; import { TokenStore } from "@stackframe/stack-shared/dist/interface/clientInterface"; +import { neverResolve } from "@stackframe/stack-shared/dist/utils/promises"; export async function signInWithOAuth( iface: StackClientInterface, @@ -22,6 +23,7 @@ export async function signInWithOAuth( state, ); window.location.assign(location); + await neverResolve(); } /** diff --git a/packages/stack/src/providers/component-provider.tsx b/packages/stack/src/providers/component-provider.tsx index 8c0c2df2c..7c81054dd 100644 --- a/packages/stack/src/providers/component-provider.tsx +++ b/packages/stack/src/providers/component-provider.tsx @@ -71,4 +71,4 @@ export function StackComponentProvider(props: { children?: React.ReactNode } & C {props.children} ); -} \ No newline at end of file +}