diff --git a/apps/dashboard/.storybook/preview.tsx b/apps/dashboard/.storybook/preview.tsx index bfba8033060..935d371869c 100644 --- a/apps/dashboard/.storybook/preview.tsx +++ b/apps/dashboard/.storybook/preview.tsx @@ -7,6 +7,7 @@ import { Inter as interFont } from "next/font/google"; // biome-ignore lint/style/useImportType: import React from "react"; import { useEffect } from "react"; +import { Toaster } from "sonner"; import { Button } from "../src/@/components/ui/button"; const queryClient = new QueryClient(); @@ -79,7 +80,13 @@ function StoryLayout(props: {
{props.children}
+ ); } + +function ToasterSetup() { + const { theme } = useTheme(); + return ; +} diff --git a/apps/dashboard/src/@/components/blocks/pricing-card.tsx b/apps/dashboard/src/@/components/blocks/pricing-card.tsx index 9884b665ab8..e716a4d5e93 100644 --- a/apps/dashboard/src/@/components/blocks/pricing-card.tsx +++ b/apps/dashboard/src/@/components/blocks/pricing-card.tsx @@ -31,7 +31,6 @@ type PricingCardProps = { ctaHint?: string; highlighted?: boolean; current?: boolean; - canTrialGrowth?: boolean; activeTrialEndsAt?: string; redirectPath: string; redirectToCheckout: RedirectBillingCheckoutAction; @@ -43,7 +42,6 @@ export const PricingCard: React.FC = ({ cta, highlighted = false, current = false, - canTrialGrowth = false, activeTrialEndsAt, redirectPath, redirectToCheckout, @@ -88,18 +86,7 @@ export const PricingCard: React.FC = ({
- {isCustomPrice ? ( - plan.price - ) : canTrialGrowth ? ( - <> - - ${plan.price} - {" "} - $0 - - ) : ( - `$${plan.price}` - )} + ${plan.price} {!isCustomPrice && ( diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts index cfb6b1c753b..cce011a40b2 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts @@ -54,7 +54,7 @@ export type Account = { // TODO - add image URL }; -interface UpdateAccountInput { +export interface UpdateAccountInput { name?: string; email?: string; linkWallet?: boolean; @@ -379,39 +379,40 @@ export function useUserOpUsagePeriod(args: { }); } -export function useUpdateAccount() { - const queryClient = useQueryClient(); - const address = useActiveAccount()?.address; +export async function updateAccountClient(input: UpdateAccountInput) { + type Result = { + data: object; + error?: { message: string }; + }; - return useMutation({ - mutationFn: async (input: UpdateAccountInput) => { - type Result = { - data: object; - error?: { message: string }; - }; + const res = await apiServerProxy({ + pathname: "/v1/account", + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); - const res = await apiServerProxy({ - pathname: "/v1/account", - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(input), - }); + if (!res.ok) { + throw new Error(res.error); + } - if (!res.ok) { - throw new Error(res.error); - } + const json = res.data; - const json = res.data; + if (json.error) { + throw new Error(json.error.message); + } - if (json.error) { - throw new Error(json.error.message); - } + return json.data; +} - return json.data; - }, +export function useUpdateAccount() { + const queryClient = useQueryClient(); + const address = useActiveAccount()?.address; + return useMutation({ + mutationFn: updateAccountClient, onSuccess: () => { return queryClient.invalidateQueries({ queryKey: accountKeys.me(address || ""), @@ -460,94 +461,61 @@ export function useUpdateNotifications() { }); } -export function useConfirmEmail() { - const address = useActiveAccount()?.address; - const queryClient = useQueryClient(); +export const verifyEmailClient = async (input: ConfirmEmailInput) => { + type Result = { + error?: { message: string }; + data: { team: Team; account: Account }; + }; - return useMutation({ - mutationFn: async (input: ConfirmEmailInput) => { - type Result = { - error?: { message: string }; - data: { team: Team; account: Account }; - }; + const res = await apiServerProxy({ + pathname: "/v1/account/confirmEmail", + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); - const res = await apiServerProxy({ - pathname: "/v1/account/confirmEmail", - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(input), - }); + if (!res.ok) { + throw new Error(res.error); + } - if (!res.ok) { - throw new Error(res.error); - } + const json = res.data; - const json = res.data; + if (json.error) { + throw new Error(json.error.message); + } - if (json.error) { - throw new Error(json.error.message); - } + return json.data; +}; - return json.data; - }, - onSuccess: async () => { - // invalidate related cache, since could be relinking account - return Promise.all([ - queryClient.invalidateQueries({ - queryKey: apiKeys.keys(address || ""), - }), - queryClient.invalidateQueries({ - queryKey: accountKeys.usage(address || ""), - }), - queryClient.invalidateQueries({ - queryKey: accountKeys.me(address || ""), - }), - ]); +export const resendEmailClient = async () => { + type Result = { + error?: { message: string }; + data: object; + }; + + const res = await apiServerProxy({ + pathname: "/v1/account/resendEmailConfirmation", + method: "POST", + headers: { + "Content-Type": "application/json", }, + body: JSON.stringify({}), }); -} - -export function useResendEmailConfirmation() { - const address = useActiveAccount()?.address; - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async () => { - type Result = { - error?: { message: string }; - data: object; - }; - - const res = await apiServerProxy({ - pathname: "/v1/account/resendEmailConfirmation", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }); - - if (!res.ok) { - throw new Error(res.error); - } + if (!res.ok) { + throw new Error(res.error); + } - const json = res.data; + const json = res.data; - if (json.error) { - throw new Error(json.error.message); - } + if (json.error) { + throw new Error(json.error.message); + } - return json.data; - }, - onSuccess: () => { - return queryClient.invalidateQueries({ - queryKey: accountKeys.me(address || ""), - }); - }, - }); -} + return json.data; +}; export function useCreateApiKey() { const address = useActiveAccount()?.address; diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/batchMetadata.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/batchMetadata.stories.tsx index 86264215994..e56bc60175b 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/batchMetadata.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/batchMetadata.stories.tsx @@ -3,7 +3,7 @@ import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import { Toaster, toast } from "sonner"; +import { toast } from "sonner"; import { BadgeContainer, mobileViewport } from "stories/utils"; import { ZERO_ADDRESS } from "thirdweb"; import { ConnectButton, ThirdwebProvider } from "thirdweb/react"; @@ -109,7 +109,6 @@ function Component() { contractChainId={1} /> -
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx index 0ebc00952f1..d0326b57128 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/claimable.stories.tsx @@ -11,7 +11,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { subDays } from "date-fns"; import { useState } from "react"; -import { Toaster, toast } from "sonner"; +import { toast } from "sonner"; import { mobileViewport } from "stories/utils"; import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "thirdweb"; import { ConnectButton, ThirdwebProvider } from "thirdweb/react"; @@ -196,8 +196,6 @@ function Component() { isValidTokenId={true} noClaimConditionSet={noClaimConditionSet} /> - -
); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx index 0b13fa91c83..20deea2edbd 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/mintable.stories.tsx @@ -11,7 +11,7 @@ import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import { Toaster, toast } from "sonner"; +import { toast } from "sonner"; import { BadgeContainer, mobileViewport } from "stories/utils"; import { ConnectButton, ThirdwebProvider } from "thirdweb/react"; import { accountStub } from "../../../../../../../stories/stubs"; @@ -175,8 +175,6 @@ function Component() { contractChainId={1} /> - - ); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.stories.tsx index 7807253299e..fae04055738 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/module-card.stories.tsx @@ -2,7 +2,6 @@ import { Checkbox } from "@/components/ui/checkbox"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import { Toaster } from "sonner"; import { BadgeContainer, mobileViewport } from "stories/utils"; import { ThirdwebProvider } from "thirdweb/react"; import { ModuleCardUI } from "./module-card"; @@ -120,8 +119,6 @@ function Component() { - - ); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/openEditionMetadata.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/openEditionMetadata.stories.tsx index 41af6473ebe..064b144fba4 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/openEditionMetadata.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/openEditionMetadata.stories.tsx @@ -3,7 +3,7 @@ import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import { Toaster, toast } from "sonner"; +import { toast } from "sonner"; import { BadgeContainer, mobileViewport } from "stories/utils"; import { ConnectButton, ThirdwebProvider } from "thirdweb/react"; import { accountStub } from "../../../../../../../stories/stubs"; @@ -98,8 +98,6 @@ function Component() { twAccount={accountStub()} /> - - ); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/royalty.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/royalty.stories.tsx index c05b1170098..564e901f115 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/royalty.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/royalty.stories.tsx @@ -3,7 +3,7 @@ import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import { Toaster, toast } from "sonner"; +import { toast } from "sonner"; import { BadgeContainer, mobileViewport } from "stories/utils"; import { ConnectButton, ThirdwebProvider } from "thirdweb/react"; import { accountStub } from "../../../../../../../stories/stubs"; @@ -168,8 +168,6 @@ function Component() { twAccount={twAccount} /> - - ); diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/transferable.stories.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/transferable.stories.tsx index 31e09341947..39c463f61d6 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/transferable.stories.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/transferable.stories.tsx @@ -3,7 +3,7 @@ import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import { Toaster, toast } from "sonner"; +import { toast } from "sonner"; import { BadgeContainer, mobileViewport } from "stories/utils"; import { ConnectButton, ThirdwebProvider } from "thirdweb/react"; import { accountStub } from "../../../../../../../stories/stubs"; @@ -139,8 +139,6 @@ function Component() { twAccount={accountStub()} /> - - ); diff --git a/apps/dashboard/src/app/account/settings/AccountSettingsPageUI.stories.tsx b/apps/dashboard/src/app/account/settings/AccountSettingsPageUI.stories.tsx index 8c6cf1df2df..a895a430773 100644 --- a/apps/dashboard/src/app/account/settings/AccountSettingsPageUI.stories.tsx +++ b/apps/dashboard/src/app/account/settings/AccountSettingsPageUI.stories.tsx @@ -2,7 +2,6 @@ import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; -import { Toaster } from "sonner"; import { mobileViewport } from "../../../stories/utils"; import { AccountSettingsPageUI } from "./AccountSettingsPageUI"; @@ -97,7 +96,6 @@ function Variants() { } }} /> - ); } diff --git a/apps/dashboard/src/app/login/LoginPage.tsx b/apps/dashboard/src/app/login/LoginPage.tsx index d01be457eb4..4e104f57048 100644 --- a/apps/dashboard/src/app/login/LoginPage.tsx +++ b/apps/dashboard/src/app/login/LoginPage.tsx @@ -3,23 +3,37 @@ import { redirectToCheckout } from "@/actions/billing"; import { getRawAccountAction } from "@/actions/getAccount"; import { ToggleThemeButton } from "@/components/color-mode-toggle"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; import { useThirdwebClient } from "@/constants/thirdweb.client"; import { useDashboardRouter } from "@/lib/DashboardRouter"; -import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { + type Account, + resendEmailClient, + updateAccountClient, + verifyEmailClient, +} from "@3rdweb-sdk/react/hooks/useApi"; import { useTheme } from "next-themes"; import Link from "next/link"; import { Suspense, lazy, useEffect, useState } from "react"; -import { ConnectEmbed, useActiveWalletConnectionStatus } from "thirdweb/react"; +import { + ConnectEmbed, + useActiveAccount, + useActiveWalletConnectionStatus, +} from "thirdweb/react"; import { createWallet, inAppWallet } from "thirdweb/wallets"; +import { apiServerProxy } from "../../@/actions/proxies"; +import type { Team } from "../../@/api/team"; import { ClientOnly } from "../../components/ClientOnly/ClientOnly"; +import { useTrack } from "../../hooks/analytics/useTrack"; import { ThirdwebMiniLogo } from "../components/ThirdwebMiniLogo"; import { getSDKTheme } from "../components/sdk-component-theme"; import { doLogin, doLogout, getLoginPayload, isLoggedIn } from "./auth-actions"; import { isOnboardingComplete } from "./onboarding/isOnboardingRequired"; +import { ConnectEmbedSizedLoadingCard } from "./onboarding/onboarding-container"; -const LazyOnboardingUI = lazy( - () => import("./onboarding/on-boarding-ui.client"), +const LazyOnboardingUI = lazy(() => import("./onboarding/on-boarding-ui")); + +const LazyShowPlansOnboarding = lazy( + () => import("./onboarding/ShowPlansOnboarding"), ); const wallets = [ @@ -104,7 +118,7 @@ export function LoginAndOnboardingPageContent(props: { - + } className="flex justify-center" @@ -126,18 +140,12 @@ export function LoginAndOnboardingPageContent(props: { ); } -function LoadingCard() { - return ( -
- -
- ); -} - function PageContent(props: { redirectPath: string; account: Account | undefined; }) { + const accountAddress = useActiveAccount()?.address; + const trackEvent = useTrack(); const [screen, setScreen] = useState< | { id: "login" } | { @@ -184,30 +192,95 @@ function PageContent(props: { }, [connectionStatus, screen.id]); if (connectionStatus === "connecting") { - return ; + return ; } - if (connectionStatus !== "connected" || screen.id === "login") { + if ( + connectionStatus !== "connected" || + screen.id === "login" || + !accountAddress + ) { return ; } if (screen.id === "onboarding") { + // when Logging in with in-app wallet, emailConfirmedAt is filled directly + // skip directly to showing the plans instead of going through the full onboarding flow + if (screen.account.emailConfirmedAt) { + return ( + }> + + + ); + } + return ( - }> + }> { - setScreen({ id: "login" }); + accountAddress={accountAddress} + loginOrSignup={async (params) => { + await updateAccountClient(params); + }} + verifyEmail={verifyEmailClient} + resendEmailConfirmation={async () => { + await resendEmailClient(); + }} + skipOnboarding={() => { + updateAccountClient({ + onboardSkipped: true, + }); + }} + trackEvent={trackEvent} + requestLinkWallet={async (email) => { + await updateAccountClient({ + email, + linkWallet: true, + }); + }} + // TODO: set this to true if the account has confirmed email + shouldSkipEmailOnboarding={false} + sendTeamOnboardingData={async (params) => { + const teamsRes = await apiServerProxy<{ + result: Team[]; + }>({ + pathname: "/v1/teams", + method: "GET", + }); + + if (!teamsRes.ok) { + throw new Error(teamsRes.error); + } + + const team = teamsRes.data.result[0]; + + if (!team) { + throw new Error("No team found"); + } + + const teamOnboardRes = await apiServerProxy({ + pathname: `/v1/teams/${team.id}/onboard`, + method: "PUT", + body: JSON.stringify(params), + }); + + if (!teamOnboardRes.ok) { + throw new Error(teamOnboardRes.error); + } }} /> ); } - return ; + return ; } function CustomConnectEmbed(props: { diff --git a/apps/dashboard/src/app/login/onboarding/AccountForm.tsx b/apps/dashboard/src/app/login/onboarding/AccountForm.tsx deleted file mode 100644 index cd597b6821c..00000000000 --- a/apps/dashboard/src/app/login/onboarding/AccountForm.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { Button } from "@/components/ui/button"; -import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; -import { type Account, useUpdateAccount } from "@3rdweb-sdk/react/hooks/useApi"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useTrack } from "hooks/analytics/useTrack"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { - type AccountValidationSchema, - accountValidationSchema, -} from "./validations"; - -interface AccountFormProps { - account: Account; - horizontal?: boolean; - showSubscription?: boolean; - hideName?: boolean; - buttonText?: string; - padded?: boolean; - trackingCategory?: string; - disableUnchanged?: boolean; - onSave?: (email: string) => void; - onDuplicateError?: (email: string) => void; -} - -export const AccountForm: React.FC = ({ - account, - onSave, - onDuplicateError, - buttonText = "Save", - hideName = false, - showSubscription = false, - disableUnchanged = false, -}) => { - const [isSubscribing, setIsSubscribing] = useState(true); - const trackEvent = useTrack(); - const form = useForm({ - resolver: zodResolver(accountValidationSchema), - defaultValues: { - name: account.name || "", - email: account.unconfirmedEmail || account.email || "", - }, - values: { - name: account.name || "", - email: account.unconfirmedEmail || account.email || "", - }, - }); - - const updateMutation = useUpdateAccount(); - - const handleSubmit = form.handleSubmit((values) => { - const formData = { - ...values, - ...(showSubscription - ? { - subscribeToUpdates: isSubscribing, - } - : {}), - }; - - trackEvent({ - category: "account", - action: "update", - label: "attempt", - data: formData, - }); - - updateMutation.mutate(formData, { - onSuccess: (data) => { - if (onSave) { - onSave(values.email); - } - - trackEvent({ - category: "account", - action: "update", - label: "success", - data, - }); - }, - onError: (error) => { - console.error(error); - - if ( - onDuplicateError && - error?.message.match(/email address already exists/) - ) { - onDuplicateError(values.email); - return; - } else if (error.message.includes("INVALID_EMAIL_ADDRESS")) { - toast.error("Invalid Email Address"); - } else { - toast.error(error.message || "Failed to update account"); - } - - trackEvent({ - category: "account", - action: "update", - label: "error", - error, - fromOnboarding: !!onDuplicateError, - }); - }, - }); - }); - - return ( -
-
-
- - - - - {!hideName && ( - - - - )} - - {showSubscription && ( - - setIsSubscribing(!!v)} - /> - Subscribe to new features and key product updates - - )} -
- - -
-
- ); -}; diff --git a/apps/dashboard/src/app/login/onboarding/ChoosePlan.tsx b/apps/dashboard/src/app/login/onboarding/ChoosePlan.tsx index 291ec711ee6..05b8a5f8b18 100644 --- a/apps/dashboard/src/app/login/onboarding/ChoosePlan.tsx +++ b/apps/dashboard/src/app/login/onboarding/ChoosePlan.tsx @@ -8,7 +8,6 @@ import { TitleAndDescription } from "./Title"; export function OnboardingChoosePlan(props: { skipPlan: () => Promise; - canTrialGrowth: boolean; teamSlug: string; redirectPath: string; redirectToCheckout: RedirectBillingCheckoutAction; @@ -48,7 +47,6 @@ export function OnboardingChoosePlan(props: { }, variant: "default", }} - canTrialGrowth={false} highlighted redirectPath={props.redirectPath} redirectToCheckout={props.redirectToCheckout} diff --git a/apps/dashboard/src/app/login/onboarding/ConfirmEmail.tsx b/apps/dashboard/src/app/login/onboarding/ConfirmEmail.tsx deleted file mode 100644 index df3cd9b05cb..00000000000 --- a/apps/dashboard/src/app/login/onboarding/ConfirmEmail.tsx +++ /dev/null @@ -1,261 +0,0 @@ -"use client"; - -import type { Team } from "@/api/team"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { Button } from "@/components/ui/button"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot, -} from "@/components/ui/input-otp"; -import { cn } from "@/lib/utils"; -import { - type Account, - useConfirmEmail, - useResendEmailConfirmation, -} from "@3rdweb-sdk/react/hooks/useApi"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useTrack } from "hooks/analytics/useTrack"; -import { useTxNotifications } from "hooks/useTxNotifications"; -import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { useActiveAccount } from "thirdweb/react"; -import { shortenString } from "utils/usedapp-external"; -import { TitleAndDescription } from "./Title"; -import { - type EmailConfirmationValidationSchema, - emailConfirmationValidationSchema, -} from "./validations"; - -interface OnboardingConfirmEmailProps { - email: string; - linking?: boolean; - onEmailConfirm: (params: { - team: Team; - account: Account; - }) => void; - onComplete: () => void; - onBack: () => void; -} - -// TODO - separate out "linking" and "confirmLinking" states into separate components - -export const OnboardingConfirmEmail: React.FC = ({ - email, - linking, - onEmailConfirm, - onBack, - onComplete, -}) => { - const [token, setToken] = useState(""); - const [completed, setCompleted] = useState(false); - const [saving, setSaving] = useState(false); - const trackEvent = useTrack(); - const address = useActiveAccount()?.address; - - const { onSuccess: onResendSuccess, onError: onResendError } = - useTxNotifications( - !linking - ? "We've sent you an email confirmation code." - : "We've sent you a wallet linking confirmation code.", - !linking - ? "Couldn't send an email confirmation code. Try later!" - : "Couldn't send a wallet linking confirmation code. Try later!", - ); - - const form = useForm({ - resolver: zodResolver(emailConfirmationValidationSchema), - defaultValues: { - confirmationToken: "", - }, - }); - - const confirmEmail = useConfirmEmail(); - const resendMutation = useResendEmailConfirmation(); - - const handleChange = (value: string) => { - setToken(value.toUpperCase()); - form.setValue("confirmationToken", value); - }; - - const handleSubmit = form.handleSubmit((values) => { - const trackingAction = !linking ? "confirmEmail" : "confirmLinkWallet"; - - setSaving(true); - - trackEvent({ - category: "account", - action: trackingAction, - label: "attempt", - }); - - confirmEmail.mutate(values, { - onSuccess: (response) => { - if (!linking) { - onEmailConfirm(response); - } else { - setCompleted(true); - } - setSaving(false); - - trackEvent({ - category: "account", - action: trackingAction, - label: "success", - }); - }, - // biome-ignore lint/suspicious/noExplicitAny: FIXME - onError: (error: any) => { - const message = - "message" in error - ? error.message - : "Couldn't verify your email address. Try later!"; - - toast.error(message); - form.reset(); - setToken(""); - setSaving(false); - - trackEvent({ - category: "account", - action: trackingAction, - label: "error", - error: message, - }); - }, - }); - }); - - const handleResend = () => { - setSaving(true); - - trackEvent({ - category: "account", - action: "resendEmailConfirmation", - label: "attempt", - }); - - resendMutation.mutate(undefined, { - onSuccess: () => { - setSaving(false); - onResendSuccess(); - - trackEvent({ - category: "account", - action: "resendEmailConfirmation", - label: "success", - }); - }, - onError: (error) => { - onResendError(error); - form.reset(); - setToken(""); - setSaving(false); - - trackEvent({ - category: "account", - action: "resendEmailConfirmation", - label: "error", - error, - }); - }, - }); - }; - - return ( - <> - - Enter the 6 letter confirmation code sent to{" "} - {email} - - ) : ( - <> - We've linked{" "} - {address && ( - {shortenString(address)} - )} - wallet to {email} thirdweb - account. - - ) - } - /> - -
- - {completed && ( - - )} - - {!completed && ( -
- - - {new Array(6).fill(0).map((_, idx) => ( - - key={idx} - index={idx} - className={cn("h-12 grow text-lg", { - "border-red-500": form.getFieldState( - "confirmationToken", - form.formState, - ).error, - })} - /> - ))} - - - -
- -
- - - - - -
- - )} - - ); -}; diff --git a/apps/dashboard/src/app/login/onboarding/General.tsx b/apps/dashboard/src/app/login/onboarding/General.tsx deleted file mode 100644 index 9eaadf20cc5..00000000000 --- a/apps/dashboard/src/app/login/onboarding/General.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { Button } from "@/components/ui/button"; -import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; -import { useMutation } from "@tanstack/react-query"; -import { useState } from "react"; -import { useActiveWallet, useDisconnect } from "thirdweb/react"; -import { doLogout } from "../auth-actions"; -import { AccountForm } from "./AccountForm"; -import { TitleAndDescription } from "./Title"; - -type OnboardingGeneralProps = { - account: Account; - onSave: (email: string) => void; - onDuplicate: (email: string) => void; - onLogout: () => void; -}; - -export const OnboardingGeneral: React.FC = ({ - account, - onSave, - onDuplicate, - onLogout, -}) => { - const [existing, setExisting] = useState(false); - const activeWallet = useActiveWallet(); - const { disconnect } = useDisconnect(); - - async function handleLogout() { - await doLogout(); - onLogout(); - if (activeWallet) { - disconnect(activeWallet); - } - } - - const logoutMutation = useMutation({ - mutationFn: handleLogout, - }); - - return ( -
- - -
- -
- - - {!existing ? ( - <> - - - - ) : ( - - )} -
-
- ); -}; diff --git a/apps/dashboard/src/app/login/onboarding/LinkWallet.tsx b/apps/dashboard/src/app/login/onboarding/LinkWallet.tsx deleted file mode 100644 index b6557ca3cc2..00000000000 --- a/apps/dashboard/src/app/login/onboarding/LinkWallet.tsx +++ /dev/null @@ -1,125 +0,0 @@ -"use client"; - -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { Button } from "@/components/ui/button"; -import { TrackedLinkTW } from "@/components/ui/tracked-link"; -import { useUpdateAccount } from "@3rdweb-sdk/react/hooks/useApi"; -import { useTrack } from "hooks/analytics/useTrack"; -import { useActiveAccount } from "thirdweb/react"; -import { shortenString } from "utils/usedapp-external"; -import { TitleAndDescription } from "./Title"; - -interface OnboardingLinkWalletProps { - email: string; - onSave: () => void; - onBack: () => void; -} - -export const OnboardingLinkWallet: React.FC = ({ - email, - onSave, - onBack, -}) => { - const address = useActiveAccount()?.address; - const trackEvent = useTrack(); - const updateMutation = useUpdateAccount(); - - const handleSubmit = () => { - trackEvent({ - category: "account", - action: "linkWallet", - label: "attempt", - data: { - email, - }, - }); - - updateMutation.mutate( - { - email, - linkWallet: true, - }, - { - onSuccess: (data) => { - if (onSave) { - onSave(); - } - - trackEvent({ - category: "account", - action: "linkWallet", - label: "success", - data, - }); - }, - onError: (err) => { - const error = err as Error; - - trackEvent({ - category: "account", - action: "linkWallet", - label: "error", - error, - }); - }, - }, - ); - }; - - return ( - <> - - We've noticed that there is another account associated with{" "} - {email}. Would you like to - link your wallet{" "} - {address && ( - - {shortenString(address)} - - )}{" "} - to the existing account? -
- Once you agree, we will email you the details.{" "} - - Learn more about wallet linking - - . - - } - /> - -
- -
-
- - -
-
- - ); -}; diff --git a/apps/dashboard/src/app/login/onboarding/LinkWalletPrompt/EmailExists.stories.tsx b/apps/dashboard/src/app/login/onboarding/LinkWalletPrompt/EmailExists.stories.tsx new file mode 100644 index 00000000000..24bdd3fbe24 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/LinkWalletPrompt/EmailExists.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + BadgeContainer, + mobileViewport, + storybookLog, +} from "../../../../stories/utils"; +import { EmailExists } from "./EmailExists"; + +const meta = { + title: "Onboarding/screens/EmailExists", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function Story() { + return ( +
+ + +
+ ); +} + +function Variant(props: { + label: string; + type: "success" | "error"; +}) { + return ( + + { + storybookLog("onLinkWalletRequestSent"); + }} + email="user@example.com" + requestLinkWallet={async (email) => { + storybookLog("linkWallet", email); + await new Promise((resolve) => setTimeout(resolve, 1000)); + if (props.type === "error") { + throw new Error("Example error"); + } + }} + onBack={() => { + storybookLog("onBack"); + }} + trackEvent={(params) => { + storybookLog("trackEvent", params); + }} + accountAddress="0x1234567890123456789012345678901234567890" + /> + + ); +} diff --git a/apps/dashboard/src/app/login/onboarding/LinkWalletPrompt/EmailExists.tsx b/apps/dashboard/src/app/login/onboarding/LinkWalletPrompt/EmailExists.tsx new file mode 100644 index 00000000000..e421dfe410d --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/LinkWalletPrompt/EmailExists.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { TrackedLinkTW } from "@/components/ui/tracked-link"; +import { useMutation } from "@tanstack/react-query"; +import { ArrowLeftIcon } from "lucide-react"; +import { toast } from "sonner"; +import { shortenString } from "utils/usedapp-external"; +import type { TrackingParams } from "../../../../hooks/analytics/useTrack"; +import { TitleAndDescription } from "../Title"; + +export function EmailExists(props: { + email: string; + accountAddress: string; + onBack: () => void; + requestLinkWallet: (email: string) => Promise; + trackEvent: (params: TrackingParams) => void; + onLinkWalletRequestSent: () => void; +}) { + const requestLinkWallet = useMutation({ + mutationFn: props.requestLinkWallet, + }); + + function handleLinkWalletRequest() { + props.trackEvent({ + category: "account", + action: "linkWallet", + label: "attempt", + data: { + email: props.email, + }, + }); + + requestLinkWallet.mutate(props.email, { + onSuccess: (data) => { + props.onLinkWalletRequestSent(); + props.trackEvent({ + category: "account", + action: "linkWallet", + label: "success", + data, + }); + }, + onError: (err) => { + const error = err as Error; + console.error(error); + toast.error("Failed to send link wallet request"); + props.trackEvent({ + category: "account", + action: "linkWallet", + label: "error", + error, + }); + }, + }); + } + + return ( +
+ + {`We've`} noticed that an account associated with{" "} + {props.email} already + exists. +
Would you like to link your wallet{" "} + + {shortenString(props.accountAddress)} + {" "} + to the existing account? +
+ + Learn more about wallet linking + + . + + } + /> + +
+ +
+ + + +
+
+ ); +} diff --git a/apps/dashboard/src/app/login/onboarding/LoginOrSignup/LoginOrSignup.stories.tsx b/apps/dashboard/src/app/login/onboarding/LoginOrSignup/LoginOrSignup.stories.tsx new file mode 100644 index 00000000000..c91542e38c6 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/LoginOrSignup/LoginOrSignup.stories.tsx @@ -0,0 +1,78 @@ +import type { UpdateAccountInput } from "@3rdweb-sdk/react/hooks/useApi"; +import type { Meta, StoryObj } from "@storybook/react"; +import { + BadgeContainer, + mobileViewport, + storybookLog, +} from "../../../../stories/utils"; +import { LoginOrSignup } from "./LoginOrSignup"; + +const meta = { + title: "Onboarding/screens/LoginOrSignup", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function loginOrSignupStug( + type: "success" | "error-generic" | "error-email-exists", +) { + return async (data: UpdateAccountInput) => { + storybookLog("loginOrSignup", data); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (type === "error-generic") { + throw new Error("Error Example"); + } + + if (type === "error-email-exists") { + throw new Error("email address already exists"); + } + }; +} + +function Story() { + return ( +
+ + + +
+ ); +} + +function Variant(props: { + label: string; + type: "success" | "error-generic" | "error-email-exists"; +}) { + return ( + + { + storybookLog("onRequestSent", params); + }} + loginOrSignup={loginOrSignupStug(props.type)} + trackEvent={(params) => { + storybookLog("trackEvent", params); + }} + /> + + ); +} diff --git a/apps/dashboard/src/app/login/onboarding/LoginOrSignup/LoginOrSignup.tsx b/apps/dashboard/src/app/login/onboarding/LoginOrSignup/LoginOrSignup.tsx new file mode 100644 index 00000000000..995ab0ace2f --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/LoginOrSignup/LoginOrSignup.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { TabButtons } from "@/components/ui/tabs"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import type { TrackingParams } from "../../../../hooks/analytics/useTrack"; +import { + type AccountValidationSchema, + accountValidationSchema, + emailSchema, +} from "../validations"; + +export function LoginOrSignup(props: { + onRequestSent: (options: { + email: string; + isExistingEmail: boolean; + }) => void; + loginOrSignup: (input: { + email: string; + subscribeToUpdates?: true; + name?: string; + }) => Promise; + trackEvent: (params: TrackingParams) => void; +}) { + const [tab, setTab] = useState<"signup" | "login">("signup"); + const loginOrSignup = useMutation({ + mutationFn: props.loginOrSignup, + }); + + function handleSubmit(values: { + email: string; + subscribeToUpdates?: true; + name?: string; + }) { + loginOrSignup.mutate(values, { + onSuccess: (data) => { + props.onRequestSent({ + email: values.email, + isExistingEmail: false, + }); + props.trackEvent({ + category: "onboarding", + action: "update", + label: "success", + data, + }); + }, + onError: (error) => { + console.error(error); + + if (error?.message.match(/email address already exists/)) { + props.onRequestSent({ + email: values.email, + isExistingEmail: true, + }); + return; + } else if (error.message.includes("INVALID_EMAIL_ADDRESS")) { + toast.error("Invalid Email Address"); + } else { + toast.error("Failed to update account"); + } + + props.trackEvent({ + category: "account", + action: "update", + label: "error", + error: error.message, + fromOnboarding: true, + }); + }, + }); + } + + return ( +
+ setTab("signup"), + isActive: tab === "signup", + isEnabled: true, + }, + { + name: "I already have an account", + onClick: () => setTab("login"), + isActive: tab === "login", + isEnabled: true, + }, + ]} + /> + +
+ + {tab === "signup" && ( + + )} + + {tab === "login" && ( + + )} +
+ ); +} + +function SignupForm(props: { + onSubmit: (values: { + name: string; + email: string; + subscribeToUpdates?: true; + }) => void; + isSubmitting: boolean; +}) { + const [subscribeToUpdates, setSubscribeToUpdates] = useState(true); + const form = useForm({ + resolver: zodResolver(accountValidationSchema), + values: { + name: "", + email: "", + }, + }); + + const handleSubmit = form.handleSubmit((values) => { + props.onSubmit({ + ...values, + ...(subscribeToUpdates + ? { + subscribeToUpdates: subscribeToUpdates, + } + : {}), + }); + }); + + return ( +
+
+
+ + + + + + + + + + setSubscribeToUpdates(!!v)} + /> + Subscribe to new features and key product updates + +
+ + +
+
+ ); +} + +const loginFormSchema = z.object({ + email: emailSchema, +}); + +type LoginFormSchema = z.infer; + +function LoginForm(props: { + onSubmit: (values: { + email: string; + }) => void; + isSubmitting: boolean; +}) { + const form = useForm({ + resolver: zodResolver(loginFormSchema), + values: { + email: "", + }, + }); + + const handleSubmit = form.handleSubmit((values) => { + props.onSubmit(values); + }); + + return ( +
+
+
+ + + +
+ + +
+
+ ); +} diff --git a/apps/dashboard/src/app/login/onboarding/ShowPlansOnboarding.tsx b/apps/dashboard/src/app/login/onboarding/ShowPlansOnboarding.tsx new file mode 100644 index 00000000000..578474d89ee --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/ShowPlansOnboarding.tsx @@ -0,0 +1,85 @@ +"use client"; + +import type { RedirectBillingCheckoutAction } from "@/actions/billing"; +import { apiServerProxy } from "@/actions/proxies"; +import type { Team } from "@/api/team"; +import { Button } from "@/components/ui/button"; +import { useQuery } from "@tanstack/react-query"; +import { OnboardingChoosePlan } from "./ChoosePlan"; +import { + ConnectEmbedSizedCard, + ConnectEmbedSizedLoadingCard, + OnboardingCard, +} from "./onboarding-container"; +import { useSkipOnboarding } from "./useSkipOnboarding"; + +function ShowPlansOnboarding(props: { + accountId: string; + onComplete: () => void; + redirectPath: string; + redirectToCheckout: RedirectBillingCheckoutAction; +}) { + const skipOnboarding = useSkipOnboarding(); + const teamsQuery = useTeams(props.accountId); + + if (teamsQuery.isPending) { + return ; + } + + const team = teamsQuery.data?.[0]; + + // should never happen - but just in case + if (!team) { + return ( + +
+

Something went wrong

+ +
+
+ ); + } + + return ( + + { + await skipOnboarding().catch(() => {}); + props.onComplete(); + }} + redirectToCheckout={props.redirectToCheckout} + /> + + ); +} + +function useTeams(accountId: string) { + return useQuery({ + queryKey: ["team", accountId], + queryFn: async () => { + const res = await apiServerProxy<{ + result: Team[]; + }>({ + pathname: "/v1/teams", + method: "GET", + }); + + if (!res.ok) { + throw new Error(res.error); + } + + return res.data.result; + }, + }); +} + +export default ShowPlansOnboarding; diff --git a/apps/dashboard/src/app/login/onboarding/TeamInfoOnboarding.stories.tsx b/apps/dashboard/src/app/login/onboarding/TeamInfoOnboarding.stories.tsx new file mode 100644 index 00000000000..c1dd1df9423 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/TeamInfoOnboarding.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { mobileViewport } from "../../../stories/utils"; +import { TeamInfoOnboarding } from "./TeamInfoOnboarding"; + +const meta = { + title: "Onboarding/screens/TeamInfoOnboarding", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function Story() { + return ( +
+ { + console.log("onComplete", params); + }} + sendTeamOnboardingData={async (formData) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("sendTeamOnboardingData", formData); + }} + /> +
+ ); +} diff --git a/apps/dashboard/src/app/login/onboarding/TeamInfoOnboarding.tsx b/apps/dashboard/src/app/login/onboarding/TeamInfoOnboarding.tsx new file mode 100644 index 00000000000..ed91a1de3d4 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/TeamInfoOnboarding.tsx @@ -0,0 +1,464 @@ +import type { Team } from "@/api/team"; +import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { + ArrowRightIcon, + CheckIcon, + Globe, + StoreIcon, + UserIcon, +} from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const teamTypes = [ + { + icon: UserIcon, + label: "Developer", + description: "I am building an app or game", + }, + { + icon: StoreIcon, + label: "Studio", + description: "I am building multiple apps or games", + }, + { + icon: Globe, + label: "Ecosystem", + description: "I am building a platform", + }, +] as const; + +const teamScales = ["Startup", "Scaleup", "Enterprise"] as const; + +const teamIndustries = [ + "Consumer", + "DeFi", + "Gaming", + "Social", + "AI", + "Blockchain", +] as const; + +const teamPlatformInterests = [ + "Connect", + "Engine", + "Contracts", + "RPC", + "Insight", + "Nebula", +] as const; + +const teamRoles = [ + "Frontend Developer", + "Backend Developer", + "Smart Contract Engineer", + "Founder", + "Product Manager", + "Business Development", + "Finance", +] as const; + +const productInterests = [ + "Connect", + "Engine", + "Contracts", + "RPC", + "Insight", + "Nebula", +] as const; + +const teamPlatforms = ["Web", "Backend", "Mobile", "Unity", "Unreal"] as const; + +type TeamType = (typeof teamTypes)[number]["label"]; + +const teamFormSchema = z.object({ + team: z.object({ + name: z.string().min(1, "Team name is required"), + type: z.enum(teamTypes.map((x) => x.label) as [TeamType, ...TeamType[]]), + scale: z.enum(teamScales), + industry: z.enum(teamIndustries), + platforms: z.array(z.enum(teamPlatforms)), + productInterests: z.array(z.enum(teamPlatformInterests)), + chainInterests: z.array(z.number()), + }), + member: z.object({ + role: z.enum(teamRoles), + }), +}); + +export type TeamOnboardingData = z.infer; + +export function TeamInfoOnboarding(props: { + onComplete: (params: { + team: Team; + account: Account; + }) => void; + sendTeamOnboardingData: (data: TeamOnboardingData) => Promise; +}) { + const sendTeamOnboardingData = useMutation({ + mutationFn: props.sendTeamOnboardingData, + }); + + const form = useForm({ + resolver: zodResolver(teamFormSchema), + defaultValues: { + team: { + productInterests: [], + platforms: [], + chainInterests: [], + }, + }, + }); + + const onSubmit = (data: TeamOnboardingData) => { + sendTeamOnboardingData.mutate(data); + }; + + // conditional fields ---- + const shouldShowPlatforms = form + .watch("team.productInterests") + .includes("Connect"); + + const showShowChainInterests = + form.watch("team.type") === "Developer" || + form.watch("team.type") === "Studio"; + + // ensure that hidden fields have empty values + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + if (!shouldShowPlatforms) { + form.setValue("team.platforms", []); + } + if (!showShowChainInterests) { + form.setValue("team.chainInterests", []); + } + }, [shouldShowPlatforms, showShowChainInterests, form]); + + return ( +
+ {/* Title + desc */} +
+

+ Tell us about your team +

+

+ This will help us personalize your experience +

+
+ +
+ + {/* Team Name */} + ( + + What is your team name? + + + + + + )} + /> + + {/* Team Type */} + ( + + What is your team type? +
+ {teamTypes.map((teamType) => ( +
+ {field.value === teamType.label && ( +
+ +
+ )} + + +
+ ))} +
+ +
+ )} + /> + + {/* Team Scale */} + ( + + What is the size of your team? + + + + )} + /> + + {/* Team Industry */} + ( + + What industry is your company in? + + + + + )} + /> + + {/* Product Interests */} + ( + + What thirdweb products made you sign up? + + + {productInterests.map((product) => ( + ( + + + { + const newValue = checked + ? [...field.value, product] + : field.value?.filter( + (value) => value !== product, + ); + field.onChange(newValue); + }} + /> + + {product} + + )} + /> + ))} + + + + + )} + /> + + {/* Platforms */} + {shouldShowPlatforms && ( + ( + + What platforms do you use? + + {teamPlatforms.map((platform) => ( + ( + + + { + const newValue = checked + ? [...field.value, platform] + : field.value?.filter( + (value) => value !== platform, + ); + field.onChange(newValue); + }} + /> + + {platform} + + )} + /> + ))} + + + + )} + /> + )} + + {/* Chain Interests */} + {showShowChainInterests && ( + ( + + + Which chains are you interested in building on? + + + + + + + )} + /> + )} + + {/* Member Role */} + ( + + What is your role in the team? + + + + )} + /> + +
+ +
+ + +
+ ); +} + +function CheckboxCard(props: { children: React.ReactNode }) { + return ( +
+
+ {props.children} +
+
+ ); +} diff --git a/apps/dashboard/src/app/login/onboarding/VerifyEmail/VerifyEmail.stories.tsx b/apps/dashboard/src/app/login/onboarding/VerifyEmail/VerifyEmail.stories.tsx new file mode 100644 index 00000000000..cfa89c453c6 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/VerifyEmail/VerifyEmail.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { newAccountStub, teamStub } from "../../../../stories/stubs"; +import { + BadgeContainer, + mobileViewport, + storybookLog, +} from "../../../../stories/utils"; +import { VerifyEmail } from "./VerifyEmail"; + +const meta = { + title: "Onboarding/screens/VerifyEmail", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function Story() { + return ( +
+ + +
+ ); +} + +function Variant(props: { + label: string; + type: "success" | "error"; +}) { + return ( + + { + await new Promise((resolve) => setTimeout(resolve, 1000)); + if (props.type === "error") { + throw new Error("Example error"); + } + return { + team: teamStub("foo", "free"), + account: newAccountStub(), + }; + }} + resendConfirmationEmail={async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + if (props.type === "error") { + throw new Error("Example error"); + } + }} + email="user@example.com" + onEmailConfirmed={(params) => { + storybookLog("onEmailConfirmed", params); + }} + onBack={() => { + storybookLog("onBack"); + }} + trackEvent={(params) => { + storybookLog("trackEvent", params); + }} + /> + + ); +} diff --git a/apps/dashboard/src/app/login/onboarding/VerifyEmail/VerifyEmail.tsx b/apps/dashboard/src/app/login/onboarding/VerifyEmail/VerifyEmail.tsx new file mode 100644 index 00000000000..19fe078bfc6 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/VerifyEmail/VerifyEmail.tsx @@ -0,0 +1,226 @@ +"use client"; + +import type { Team } from "@/api/team"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@/components/ui/input-otp"; +import { cn } from "@/lib/utils"; +import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import type { TrackingParams } from "hooks/analytics/useTrack"; +import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; +import { ArrowLeftIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { TitleAndDescription } from "../Title"; +import { + type EmailConfirmationValidationSchema, + emailConfirmationValidationSchema, +} from "../validations"; + +type VerifyEmailProps = { + email: string; + onEmailConfirmed: (params: { + team: Team; + account: Account; + }) => void; + onBack: () => void; + verifyEmail: (params: { + confirmationToken: string; + }) => Promise<{ + team: Team; + account: Account; + }>; + resendConfirmationEmail: () => Promise; + trackEvent: (params: TrackingParams) => void; + accountAddress: string; + title: string; + trackingAction: string; +}; + +export function VerifyEmail(props: VerifyEmailProps) { + const form = useForm({ + resolver: zodResolver(emailConfirmationValidationSchema), + values: { + confirmationToken: "", + }, + }); + + const verifyEmail = useMutation({ + mutationFn: props.verifyEmail, + }); + + const resendConfirmationEmail = useMutation({ + mutationFn: props.resendConfirmationEmail, + }); + + const handleSubmit = form.handleSubmit((values) => { + props.trackEvent({ + category: "account", + action: props.trackingAction, + label: "attempt", + }); + + verifyEmail.mutate(values, { + onSuccess: (response) => { + props.onEmailConfirmed(response); + props.trackEvent({ + category: "account", + action: props.trackingAction, + label: "success", + }); + }, + onError: (error) => { + console.error(error); + toast.error("Invalid confirmation code"); + props.trackEvent({ + category: "account", + action: props.trackingAction, + label: "error", + error: error.message, + }); + }, + }); + }); + + function handleResend() { + form.setValue("confirmationToken", ""); + verifyEmail.reset(); + + props.trackEvent({ + category: "account", + action: "resendEmailConfirmation", + label: "attempt", + }); + + resendConfirmationEmail.mutate(undefined, { + onSuccess: () => { + toast.success("Verification code sent"); + props.trackEvent({ + category: "account", + action: "resendEmailConfirmation", + label: "success", + }); + }, + onError: (error) => { + toast.error("Failed to send verification code"); + props.trackEvent({ + category: "account", + action: "resendEmailConfirmation", + label: "error", + error, + }); + }, + }); + } + + return ( +
+ + Enter the 6 letter confirmation code sent to{" "} + {props.email} + + } + /> + +
+ +
+ { + form.setValue("confirmationToken", otp); + }} + disabled={verifyEmail.isPending} + > + + {new Array(6).fill(0).map((_, idx) => ( + + key={idx} + index={idx} + className={cn("h-12 grow text-xl", { + "border-red-500": + form.getFieldState("confirmationToken", form.formState) + .error || verifyEmail.isError, + })} + /> + ))} + + + +
+ +
+ + + + +
+ +
+
+ +
+ ); +} + +export function LinkWalletVerifyEmail( + props: Omit, +) { + return ( + + ); +} + +export function SignupVerifyEmail( + props: Omit, +) { + return ( + + ); +} diff --git a/apps/dashboard/src/app/login/onboarding/on-boarding-ui.client.tsx b/apps/dashboard/src/app/login/onboarding/on-boarding-ui.client.tsx deleted file mode 100644 index 85467d12253..00000000000 --- a/apps/dashboard/src/app/login/onboarding/on-boarding-ui.client.tsx +++ /dev/null @@ -1,151 +0,0 @@ -"use client"; -import type { RedirectBillingCheckoutAction } from "@/actions/billing"; -import type { Team } from "@/api/team"; -import { cn } from "@/lib/utils"; -import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; -import { useTrack } from "hooks/analytics/useTrack"; -import { useState } from "react"; -import { OnboardingChoosePlan } from "./ChoosePlan"; -import { OnboardingConfirmEmail } from "./ConfirmEmail"; -import { OnboardingGeneral } from "./General"; -import { OnboardingLinkWallet } from "./LinkWallet"; -import { useSkipOnboarding } from "./useSkipOnboarding"; - -type OnboardingScreen = - | { id: "onboarding" } - | { id: "linking" } - | { id: "confirming" } - | { id: "confirmLinking" } - | { id: "plan"; team: Team }; - -function OnboardingUI(props: { - account: Account; - onComplete: () => void; - onLogout: () => void; - // path to redirect from stripe - redirectPath: string; - redirectToCheckout: RedirectBillingCheckoutAction; -}) { - const { account } = props; - const [screen, setScreen] = useState({ id: "onboarding" }); - - const trackEvent = useTrack(); - const [updatedEmail, setUpdatedEmail] = useState(); - const skipOnboarding = useSkipOnboarding(); - - function trackOnboardingStep(params: { - nextStep: OnboardingScreen["id"]; - email?: string; - }) { - trackEvent({ - category: "account", - action: "onboardingStep", - label: "next", - data: { - email: params.email || account.unconfirmedEmail || updatedEmail, - currentStep: screen, - nextStep: params.nextStep, - }, - }); - } - - const handleDuplicateEmail = (email: string) => { - setScreen({ - id: "linking", - }); - trackOnboardingStep({ - nextStep: "linking", - email, - }); - }; - - return ( -
- {screen.id === "onboarding" && ( - { - setUpdatedEmail(email); - setScreen({ - id: "confirming", - }); - trackOnboardingStep({ - nextStep: "confirming", - email, - }); - }} - onDuplicate={(email) => { - setUpdatedEmail(email); - handleDuplicateEmail(email); - }} - /> - )} - - {screen.id === "linking" && ( - { - setScreen({ - id: "confirmLinking", - }); - trackOnboardingStep({ - nextStep: "confirmLinking", - }); - }} - onBack={() => { - setUpdatedEmail(undefined); - setScreen({ - id: "onboarding", - }); - }} - email={updatedEmail as string} - /> - )} - - {/* TODO - separate the confirming and confirmLinking into separate components */} - {(screen.id === "confirming" || screen.id === "confirmLinking") && ( - { - if (screen.id === "confirmLinking") { - props.onComplete(); - } else if (screen.id === "confirming") { - if (account.onboardSkipped) { - props.onComplete(); - } else { - setScreen({ id: "plan", team: res.team }); - } - } - }} - onBack={() => - setScreen({ - id: "onboarding", - }) - } - email={(account.unconfirmedEmail || updatedEmail) as string} - /> - )} - - {screen.id === "plan" && ( - { - await skipOnboarding().catch(() => {}); - props.onComplete(); - }} - canTrialGrowth={true} - redirectToCheckout={props.redirectToCheckout} - /> - )} -
- ); -} - -export default OnboardingUI; diff --git a/apps/dashboard/src/app/login/onboarding/on-boarding-ui.tsx b/apps/dashboard/src/app/login/onboarding/on-boarding-ui.tsx new file mode 100644 index 00000000000..1d37f0b5941 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/on-boarding-ui.tsx @@ -0,0 +1,212 @@ +"use client"; + +import type { RedirectBillingCheckoutAction } from "@/actions/billing"; +import type { Team } from "@/api/team"; +import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { useState } from "react"; +import type { TrackingParams } from "../../../hooks/analytics/useTrack"; +import { OnboardingChoosePlan } from "./ChoosePlan"; +import { EmailExists } from "./LinkWalletPrompt/EmailExists"; +import { LoginOrSignup } from "./LoginOrSignup/LoginOrSignup"; +import { + TeamInfoOnboarding, + type TeamOnboardingData, +} from "./TeamInfoOnboarding"; +import { + LinkWalletVerifyEmail, + SignupVerifyEmail, +} from "./VerifyEmail/VerifyEmail"; + +type EmailOnboardingScreen = + | { id: "login-or-signup" } + | { id: "email-exists"; email: string; backScreen: EmailOnboardingScreen } + | { + id: "signup-verify-email"; + email: string; + backScreen: EmailOnboardingScreen; + } + | { + id: "link-wallet-verify-email"; + email: string; + backScreen: EmailOnboardingScreen; + }; + +type EmailOnboardingProps = { + onComplete: (param: { + team: Team; + account: Account; + }) => void; + accountAddress: string; + trackEvent: (params: TrackingParams) => void; + verifyEmail: (params: { + confirmationToken: string; + }) => Promise<{ + team: Team; + account: Account; + }>; + resendEmailConfirmation: () => Promise; + loginOrSignup: (input: { + email: string; + subscribeToUpdates?: true; + name?: string; + }) => Promise; + requestLinkWallet: (email: string) => Promise; +}; + +function EmailOnboarding(props: EmailOnboardingProps) { + const [screen, setScreen] = useState({ + id: "login-or-signup", + }); + + return ( +
+ {screen.id === "login-or-signup" && ( + { + if (params.isExistingEmail) { + setScreen({ + id: "email-exists", + email: params.email, + backScreen: screen, + }); + } else { + setScreen({ + id: "signup-verify-email", + email: params.email, + backScreen: screen, + }); + } + }} + /> + )} + + {screen.id === "email-exists" && ( + { + setScreen({ + id: "link-wallet-verify-email", + email: screen.email, + backScreen: screen, + }); + }} + onBack={() => setScreen(screen.backScreen)} + email={screen.email} + /> + )} + + {screen.id === "signup-verify-email" && ( + { + props.onComplete({ + team: data.team, + account: data.account, + }); + }} + onBack={() => setScreen(screen.backScreen)} + email={screen.email} + /> + )} + + {screen.id === "link-wallet-verify-email" && ( + setScreen(screen.backScreen)} + email={screen.email} + /> + )} +
+ ); +} + +type OnboardingProps = EmailOnboardingProps & { + redirectPath: string; + redirectToCheckout: RedirectBillingCheckoutAction; + skipOnboarding: () => void; + shouldSkipEmailOnboarding: boolean; + sendTeamOnboardingData: (data: TeamOnboardingData) => Promise; +}; + +type OnboardingScreen = + | { + id: "email-onboarding"; + } + | { + id: "show-plans"; + team: Team; + account: Account; + } + | { + id: "get-team-info"; + }; + +function Onboarding(props: OnboardingProps) { + const [screen, setScreen] = useState({ + id: props.shouldSkipEmailOnboarding ? "get-team-info" : "email-onboarding", + }); + + return ( +
+ {/* 1. */} + {screen.id === "email-onboarding" && ( + { + setScreen({ + id: "get-team-info", + }); + }} + /> + )} + + {/* 2. */} + {screen.id === "get-team-info" && ( + { + setScreen({ + id: "show-plans", + team: params.team, + account: params.account, + }); + }} + /> + )} + + {/* 3. */} + {screen.id === "show-plans" && ( + { + props.skipOnboarding(); + props.onComplete({ + team: screen.team, + account: screen.account, + }); + }} + redirectToCheckout={props.redirectToCheckout} + /> + )} +
+ ); +} + +export default Onboarding; diff --git a/apps/dashboard/src/app/login/onboarding/onboarding-container.tsx b/apps/dashboard/src/app/login/onboarding/onboarding-container.tsx new file mode 100644 index 00000000000..8afbda6be52 --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/onboarding-container.tsx @@ -0,0 +1,36 @@ +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { cn } from "@/lib/utils"; + +export function OnboardingCard(props: { + large?: boolean; + children: React.ReactNode; +}) { + return ( +
+ {props.children} +
+ ); +} + +export function ConnectEmbedSizedCard(props: { + children: React.ReactNode; +}) { + return ( +
+ {props.children} +
+ ); +} + +export function ConnectEmbedSizedLoadingCard() { + return ( + + + + ); +} diff --git a/apps/dashboard/src/app/login/onboarding/onboarding-ui.stories.tsx b/apps/dashboard/src/app/login/onboarding/onboarding-ui.stories.tsx new file mode 100644 index 00000000000..1a982adae8a --- /dev/null +++ b/apps/dashboard/src/app/login/onboarding/onboarding-ui.stories.tsx @@ -0,0 +1,131 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { newAccountStub, teamStub } from "../../../stories/stubs"; +import { + BadgeContainer, + mobileViewport, + storybookLog, +} from "../../../stories/utils"; +import Onboarding from "./on-boarding-ui"; + +const meta = { + title: "Onboarding/Flow", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: mobileViewport("iphone14"), + }, +}; + +function Story() { + return ( +
+ {/* Send Email */} + + + + + + + +
+ ); +} + +function Variant(props: { + label: string; + loginOrSignupType: "success" | "error-email-exists" | "error-generic"; + requestLinkWalletType: "success" | "error"; + verifyEmailType: "success" | "error"; +}) { + return ( + + Promise.resolve({ status: 200 })} + skipOnboarding={() => { + storybookLog("skipOnboarding"); + }} + onComplete={() => { + storybookLog("onComplete"); + }} + accountAddress="" + trackEvent={(params) => { + storybookLog("trackEvent", params); + }} + loginOrSignup={async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (props.loginOrSignupType === "error-email-exists") { + throw new Error("email address already exists"); + } + + if (props.loginOrSignupType === "error-generic") { + throw new Error("generic error"); + } + }} + requestLinkWallet={async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (props.requestLinkWalletType === "error") { + throw new Error("generic error"); + } + }} + verifyEmail={async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (props.verifyEmailType === "error") { + throw new Error("generic error"); + } + + return { + team: teamStub("foo", "free"), + account: newAccountStub(), + }; + }} + resendEmailConfirmation={async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }} + sendTeamOnboardingData={async (params) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + storybookLog("sendTeamOnboardingData", params); + }} + /> + + ); +} diff --git a/apps/dashboard/src/app/login/onboarding/validations.ts b/apps/dashboard/src/app/login/onboarding/validations.ts index 60e94401990..2ca98a57a7c 100644 --- a/apps/dashboard/src/app/login/onboarding/validations.ts +++ b/apps/dashboard/src/app/login/onboarding/validations.ts @@ -6,12 +6,12 @@ const nameValidation = z .min(3, { message: "Must be at least 3 chars" }) .max(64, { message: "Must be max 64 chars" }); -const emailValidation = z.string().refine((str) => RE_EMAIL.test(str), { +export const emailSchema = z.string().refine((str) => RE_EMAIL.test(str), { message: "Email address is not valid", }); export const accountValidationSchema = z.object({ - email: emailValidation, + email: emailSchema, name: nameValidation.or(z.literal("")), }); diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx index 07b1f6108a1..440698cac21 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx @@ -1,5 +1,4 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Toaster } from "sonner"; import { ThirdwebProvider } from "thirdweb/react"; import { accountStub, randomLorem } from "../../../../stories/stubs"; import { BadgeContainer, mobileViewport } from "../../../../stories/utils"; @@ -80,7 +79,6 @@ function Variant(props: { CHILDREN
- ); } diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx index 669c570bd70..faf7961c929 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx @@ -1,6 +1,5 @@ import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; -import { Toaster } from "sonner"; import { ConnectButton, ThirdwebProvider } from "thirdweb/react"; import { accountStub, randomLorem } from "../../../../stories/stubs"; import { BadgeContainer, mobileViewport } from "../../../../stories/utils"; @@ -245,8 +244,6 @@ function Story() { ]} /> - -
); diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx index 761366e5e51..b3563ab21aa 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; -import { Toaster } from "sonner"; import { BadgeContainer, mobileViewport } from "../../../../stories/utils"; import type { NebulaContext } from "../api/chat"; import ContextFiltersButton from "./ContextFilters"; @@ -65,7 +64,6 @@ function Story() { }} label="chains + wallet" /> -
); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/ManageEngineAlerts.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/ManageEngineAlerts.stories.tsx index 025988101f4..2b91d0d4841 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/ManageEngineAlerts.stories.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/ManageEngineAlerts.stories.tsx @@ -4,7 +4,6 @@ import type { } from "@3rdweb-sdk/react/hooks/useEngine"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; -import { Toaster } from "sonner"; import { createEngineAlertRuleStub, createEngineNotificationChannelStub, @@ -125,7 +124,6 @@ function Story() { deleteAlert={deleteAlert} /> -
); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/RecentEngineAlerts.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/RecentEngineAlerts.stories.tsx index 3d02a07fc44..b154958758e 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/RecentEngineAlerts.stories.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/alerts/components/RecentEngineAlerts.stories.tsx @@ -1,5 +1,4 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Toaster } from "sonner"; import { createEngineAlertRuleStub, createEngineAlertStub, @@ -68,8 +67,6 @@ function Story() { onAlertsUpdated={() => {}} /> - -
); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/GeneralSettingsPage.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/GeneralSettingsPage.stories.tsx index 2c6ea9c6747..973f5cae758 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/GeneralSettingsPage.stories.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/GeneralSettingsPage.stories.tsx @@ -1,4 +1,3 @@ -import { Toaster } from "@/components/ui/sonner"; import { getThirdwebClient } from "@/constants/thirdweb.server"; import type { Meta, StoryObj } from "@storybook/react"; import { teamStub } from "../../../../../../../stories/stubs"; @@ -50,7 +49,6 @@ function Story() { client={getThirdwebClient()} /> -
); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/members/TeamMembersSettingsPage.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/members/TeamMembersSettingsPage.stories.tsx index c6b5c3ef156..4bf118946d3 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/members/TeamMembersSettingsPage.stories.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/members/TeamMembersSettingsPage.stories.tsx @@ -1,5 +1,4 @@ import type { TeamAccountRole, TeamMember } from "@/api/team-members"; -import { Toaster } from "@/components/ui/sonner"; import type { Meta, StoryObj } from "@storybook/react"; import { teamStub } from "../../../../../../../stories/stubs"; import { @@ -78,8 +77,6 @@ function Story() { /> - -
); } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx index af88097f2de..cc24e22558b 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx @@ -1,7 +1,6 @@ import type { UpdateKeyInput } from "@3rdweb-sdk/react/hooks/useApi"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; -import { Toaster } from "sonner"; import { createApiKeyStub } from "../../../../../stories/stubs"; import { mobileViewport } from "../../../../../stories/utils"; import { ProjectGeneralSettingsPageUI } from "./ProjectGeneralSettingsPage"; @@ -72,8 +71,6 @@ function Story() { }} showNebulaSettings={false} /> - -
); } diff --git a/apps/dashboard/src/components/settings/Account/Billing/ApplyCouponCard.stories.tsx b/apps/dashboard/src/components/settings/Account/Billing/ApplyCouponCard.stories.tsx index adfc2faf100..708d6cff30e 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/ApplyCouponCard.stories.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/ApplyCouponCard.stories.tsx @@ -1,5 +1,4 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { Toaster } from "sonner"; import { BadgeContainer, mobileViewport } from "../../../../stories/utils"; import { type ActiveCouponResponse, ApplyCouponCardUI } from "./CouponCard"; @@ -118,7 +117,6 @@ function Story() { isPaymentSetup={true} /> -
); } diff --git a/apps/dashboard/src/components/settings/Account/Billing/CouponDetails.stories.tsx b/apps/dashboard/src/components/settings/Account/Billing/CouponDetails.stories.tsx index 63860ab37f7..1a19dcf5bdc 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/CouponDetails.stories.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/CouponDetails.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; import { useState } from "react"; -import { Toaster } from "sonner"; import { mobileViewport } from "../../../../stories/utils"; import { CouponDetailsCardUI } from "./CouponCard"; @@ -53,8 +52,6 @@ function Story() { isPending: isPending, }} /> - -
); } diff --git a/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx b/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx index 04c1522450e..4946b0410db 100644 --- a/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx +++ b/apps/dashboard/src/components/settings/Account/Billing/Pricing.tsx @@ -143,7 +143,6 @@ export const BillingPricing: React.FC = ({ } : undefined } - canTrialGrowth={false} // upsell growth plan if user is on free plan highlighted={validTeamPlan === "free" || validTeamPlan === "starter"} teamSlug={team.slug} diff --git a/apps/dashboard/src/components/settings/ApiKeys/Create/CreateApiKeyModal.stories.tsx b/apps/dashboard/src/components/settings/ApiKeys/Create/CreateApiKeyModal.stories.tsx index c5055147494..8bcdc449a5b 100644 --- a/apps/dashboard/src/components/settings/ApiKeys/Create/CreateApiKeyModal.stories.tsx +++ b/apps/dashboard/src/components/settings/ApiKeys/Create/CreateApiKeyModal.stories.tsx @@ -3,7 +3,6 @@ import type { CreateKeyInput } from "@3rdweb-sdk/react/hooks/useApi"; import type { Meta, StoryObj } from "@storybook/react"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; -import { Toaster } from "sonner"; import { CreateAPIKeyDialogUI, type CreateAPIKeyPrefillOptions } from "."; import { createApiKeyStub } from "../../../../stories/stubs"; import { mobileViewport } from "../../../../stories/utils"; @@ -64,8 +63,6 @@ function Story(props: { > Open - - ); } diff --git a/apps/dashboard/src/hooks/analytics/useTrack.ts b/apps/dashboard/src/hooks/analytics/useTrack.ts index b5461ec8269..16eefc5b746 100644 --- a/apps/dashboard/src/hooks/analytics/useTrack.ts +++ b/apps/dashboard/src/hooks/analytics/useTrack.ts @@ -2,7 +2,7 @@ import { flatten } from "flat"; import posthog from "posthog-js"; import { useCallback } from "react"; -type TExtendedTrackParams = { +export type TrackingParams = { category: string; action: string; label?: string; @@ -11,7 +11,7 @@ type TExtendedTrackParams = { }; export function useTrack() { - return useCallback((trackingData: TExtendedTrackParams) => { + return useCallback((trackingData: TrackingParams) => { const { category, action, label, ...restData } = trackingData; const catActLab = label ? `${category}.${action}.${label}` diff --git a/apps/dashboard/src/stories/stubs.ts b/apps/dashboard/src/stories/stubs.ts index 893f1cadd5a..252e36a536a 100644 --- a/apps/dashboard/src/stories/stubs.ts +++ b/apps/dashboard/src/stories/stubs.ts @@ -318,3 +318,15 @@ export function accountStub(overrides?: Partial): Account { ...overrides, }; } + +export function newAccountStub(overrides?: Partial): Account { + return { + email: undefined, + name: undefined, + id: "foo", + isStaff: false, + advancedEnabled: false, + creatorWalletAddress: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37", + ...overrides, + }; +} diff --git a/apps/dashboard/src/stories/utils.tsx b/apps/dashboard/src/stories/utils.tsx index 7b28d886321..ecd4e9fb19a 100644 --- a/apps/dashboard/src/stories/utils.tsx +++ b/apps/dashboard/src/stories/utils.tsx @@ -34,3 +34,14 @@ export function mobileViewport( defaultViewport: key, }; } + +export function storybookLog( + ...mesages: (string | object | number | boolean)[] +) { + // custom log style + console.log( + "%cStorybook", + "color: white; background-color: black; padding: 2px 4px; border-radius: 4px;", + ...mesages, + ); +}