Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1a6574c22 | ||
|
|
98f358d63a | ||
|
|
db5457fa87 | ||
|
|
df0388d83b | ||
|
|
9685a85274 | ||
|
|
cd2c0f3868 | ||
|
|
4f346e69e3 | ||
|
|
f9f536568e | ||
|
|
79f029cdae | ||
|
|
2914a50b84 | ||
|
|
1b559bf224 | ||
|
|
982219a939 | ||
|
|
bed3561070 | ||
|
|
99adbe24ca | ||
|
|
d7e34f23e6 | ||
|
|
e06cc9b920 | ||
|
|
ec5336695b | ||
|
|
e7dd1864f9 | ||
|
|
ba9a7d4148 | ||
|
|
c776943d4e | ||
|
|
abcbcd0b64 | ||
|
|
3d201fc603 | ||
|
|
f3fee565f9 | ||
|
|
ec0f8d20bb | ||
|
|
ccd4cd6f59 | ||
|
|
bd0dbb7033 | ||
|
|
f5b82308ee | ||
|
|
7c103431c4 | ||
|
|
6a4b776ecc | ||
|
|
e29d80a9ec | ||
|
|
0c5b0bf007 | ||
|
|
9adf6e6b25 | ||
|
|
9330401645 |
Vendored
+3
@@ -21,5 +21,8 @@
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
*/
|
||||
async function fetchChainsFromApi() {
|
||||
const res = await fetch(`https://api.thirdweb.com/v1/chains`, {
|
||||
const res = await fetch("https://api.thirdweb.com/v1/chains", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -55,7 +55,9 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
exclude: ["/chain/validate"],
|
||||
transform: async (config, path) => {
|
||||
transform: async (config, _path) => {
|
||||
let path = _path;
|
||||
|
||||
// ignore og image paths
|
||||
if (path.includes("_og")) {
|
||||
return null;
|
||||
|
||||
@@ -40,7 +40,11 @@ const securityHeaders = [
|
||||
const redirects = require("./redirects");
|
||||
|
||||
// add framer paths here
|
||||
const FRAMER_PATHS = ["/connect/sign-in", "/contracts/modular-contracts"];
|
||||
const FRAMER_PATHS = [
|
||||
"/connect/sign-in",
|
||||
"/contracts/modular-contracts",
|
||||
"/unlimited-wallets",
|
||||
];
|
||||
|
||||
/**
|
||||
* @returns {import('next').RemotePattern[]}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
|
||||
const TeamAccountRole = {
|
||||
OWNER: "OWNER",
|
||||
@@ -26,14 +25,10 @@ export type TeamMember = {
|
||||
};
|
||||
|
||||
export async function getMembers(teamSlug: string) {
|
||||
const cookiesManager = cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
@@ -44,8 +39,10 @@ export async function getMembers(teamSlug: string) {
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (teamsRes.ok) {
|
||||
return (await teamsRes.json())?.result as TeamMember[];
|
||||
}
|
||||
return [];
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Sidebar(props: SidebarContentProps) {
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="pt-7">
|
||||
<div className="py-7">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-1">
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
|
||||
@@ -159,12 +159,6 @@ function useUnderline<El extends HTMLElement>() {
|
||||
setTimeout(() => {
|
||||
lineEl.style.transition = "transform 0.3s, width 0.3s";
|
||||
}, 0);
|
||||
|
||||
activeTabEl.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
} else if (lineRef.current) {
|
||||
lineRef.current.style.width = "0px";
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
|
||||
/* Others */
|
||||
--radius: 0.5rem;
|
||||
--chart-lightness: 50%;
|
||||
--chart-saturation: 50%;
|
||||
}
|
||||
|
||||
.dark,
|
||||
@@ -78,9 +76,6 @@
|
||||
--border: 0 0% 15%;
|
||||
--ring: 0 0% 30%;
|
||||
--input: 0 0% 15%;
|
||||
|
||||
--chart-lightness: 50%;
|
||||
--chart-saturation: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,7 @@ export function useLoggedInUser(): {
|
||||
retry: false,
|
||||
queryFn: async () => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (pathname) {
|
||||
searchParams.set("pathname", pathname);
|
||||
}
|
||||
|
||||
if (connectedAddress) {
|
||||
searchParams.set("address", connectedAddress);
|
||||
}
|
||||
@@ -76,10 +74,18 @@ export function useLoggedInUser(): {
|
||||
// legit use-case for now
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (query.data?.redirectTo) {
|
||||
router.replace(query.data.redirectTo);
|
||||
if (query.data?.isLoggedIn === false) {
|
||||
// not using useSearchParams hook here to avoid adding Suspense boundaries everywhere this hook is being used
|
||||
const currentHref = new URL(window.location.href);
|
||||
const currentPathname = currentHref.pathname;
|
||||
const currentSearchParams = currentHref.searchParams.toString();
|
||||
router.replace(
|
||||
buildLoginPath(
|
||||
`${currentPathname}${currentSearchParams ? `?${currentSearchParams}` : ""}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [query.data?.redirectTo, router]);
|
||||
}, [query.data?.isLoggedIn, router]);
|
||||
|
||||
// if we are "disconnected" we are not logged in
|
||||
if (connectionStatus === "disconnected") {
|
||||
@@ -135,3 +141,7 @@ export function useLoggedInUser(): {
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildLoginPath(pathname: string | undefined): string {
|
||||
return `/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function getChain(
|
||||
).then((res) => res.json()) as Promise<{ data: ChainServices }>,
|
||||
]);
|
||||
|
||||
if (!chain.data) {
|
||||
if (!chain.data || !chainServices.data) {
|
||||
notFound();
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { ErrorProvider } from "../../contexts/error-handler";
|
||||
import { TryTeamsUIBanner } from "../components/DashboardTypeBanner";
|
||||
import { DashboardHeader } from "../components/Header/DashboardHeader";
|
||||
|
||||
export default function DashboardLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ErrorProvider>
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<TryTeamsUIBanner />
|
||||
<DashboardHeader />
|
||||
<main className="grow">{props.children}</main>
|
||||
<AppFooter />
|
||||
|
||||
@@ -4,7 +4,6 @@ import { DashboardTypeCookieSetter } from "@/components/DashboardTypeCookieSette
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import type React from "react";
|
||||
import { TeamsUIBanner } from "../components/DashboardTypeBanner";
|
||||
import { TWAutoConnect } from "../components/autoconnect";
|
||||
import { AccountHeader } from "./components/AccountHeader";
|
||||
|
||||
@@ -13,7 +12,6 @@ export default async function AccountLayout(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<TeamsUIBanner />
|
||||
<div className="flex grow flex-col">
|
||||
<HeaderAndNav />
|
||||
<div className="border-border border-b py-10">
|
||||
|
||||
@@ -4,20 +4,13 @@ import { cookies } from "next/headers";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getAddress } from "thirdweb/utils";
|
||||
|
||||
export type EnsureLoginPayload = {
|
||||
pathname: string;
|
||||
address?: string;
|
||||
};
|
||||
|
||||
export type EnsureLoginResponse = {
|
||||
isLoggedIn: boolean;
|
||||
jwt?: string;
|
||||
redirectTo?: string;
|
||||
};
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
const address = req.nextUrl.searchParams.get("address");
|
||||
const pathname = req.nextUrl.searchParams.get("pathname");
|
||||
|
||||
const cookieStore = cookies();
|
||||
// if we are "disconnected" we are not logged in, clear the cookie and redirect to login
|
||||
@@ -34,7 +27,6 @@ export const GET = async (req: NextRequest) => {
|
||||
cookieStore.delete(COOKIE_ACTIVE_ACCOUNT);
|
||||
return NextResponse.json({
|
||||
isLoggedIn: false,
|
||||
redirectTo: buildLoginPath(pathname),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +41,6 @@ export const GET = async (req: NextRequest) => {
|
||||
cookieStore.delete(COOKIE_ACTIVE_ACCOUNT);
|
||||
return NextResponse.json({
|
||||
isLoggedIn: false,
|
||||
redirectTo: buildLoginPath(pathname),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +56,6 @@ export const GET = async (req: NextRequest) => {
|
||||
cookieStore.delete(authCookieName);
|
||||
return NextResponse.json({
|
||||
isLoggedIn: false,
|
||||
redirectTo: buildLoginPath(pathname),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,7 +77,3 @@ export const GET = async (req: NextRequest) => {
|
||||
// if everything is good simply return true
|
||||
return NextResponse.json({ isLoggedIn: true, jwt: token });
|
||||
};
|
||||
|
||||
function buildLoginPath(pathname?: string | null): string {
|
||||
return `/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AnnouncementBanner } from "../../components/notices/AnnouncementBanner";
|
||||
|
||||
export function TeamsUIBanner() {
|
||||
return (
|
||||
<AnnouncementBanner
|
||||
label="You are exploring the new dashboard. Click here to go back to the legacy dashboard."
|
||||
href="/dashboard"
|
||||
trackingLabel="old-dashboard"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TryTeamsUIBanner() {
|
||||
return (
|
||||
<AnnouncementBanner
|
||||
label="Explore the new teams dashboard. Now in Beta."
|
||||
href="/team"
|
||||
trackingLabel="team-dashboard"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NavLink } from "@/components/ui/NavLink";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { CmdKSearch } from "components/cmd-k-search";
|
||||
@@ -25,6 +26,19 @@ export const DashboardHeader: React.FC = () => {
|
||||
|
||||
{/* Right */}
|
||||
<div className="flex items-center gap-2">
|
||||
<NavLink
|
||||
href="/team"
|
||||
tracking={{
|
||||
category: "header",
|
||||
action: "click",
|
||||
label: "team-dashboard",
|
||||
}}
|
||||
className="hidden items-center gap-1.5 px-2.5 text-muted-foreground text-sm hover:text-foreground md:inline-flex"
|
||||
activeClassName="text-foreground"
|
||||
>
|
||||
Team Dashboard <Badge className="px-1.5"> BETA </Badge>
|
||||
</NavLink>
|
||||
|
||||
<div className="hidden gap-2 md:flex">
|
||||
<Suspense fallback={null}>
|
||||
<CreditsButton />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { ArrowUpDownIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { ResourcesDropdownButton } from "./ResourcesDropdownButton";
|
||||
@@ -11,6 +13,23 @@ export function SecondaryNav(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-6">
|
||||
<ToolTipLabel
|
||||
label={
|
||||
<span>
|
||||
You are exploring the new dashboard. <br /> Click here to go back to
|
||||
the legacy dashboard.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
<ArrowUpDownIcon className="size-4" />
|
||||
Switch To Legacy Dashboard
|
||||
</Link>
|
||||
</ToolTipLabel>
|
||||
|
||||
<ResourcesDropdownButton />
|
||||
|
||||
<Link
|
||||
|
||||
@@ -7,6 +7,7 @@ import PlausibleProvider from "next-plausible";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Inter } from "next/font/google";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { UnlimitedWalletsBanner } from "../components/notices/AnnouncementBanner";
|
||||
import { PostHogProvider } from "./components/root-providers";
|
||||
import { AppRouterProviders } from "./providers";
|
||||
|
||||
@@ -70,6 +71,7 @@ export default function RootLayout({
|
||||
)}
|
||||
>
|
||||
<PostHogPageView />
|
||||
<UnlimitedWalletsBanner />
|
||||
<AppRouterProviders>{children}</AppRouterProviders>
|
||||
<Toaster richColors />
|
||||
<DashboardRouterTopProgressBar />
|
||||
|
||||
@@ -15,7 +15,7 @@ import { doLogin, doLogout, getLoginPayload, isLoggedIn } from "./auth-actions";
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="relative grid h-screen place-items-center bg-muted/30">
|
||||
<nav className="fixed top-0 z-20 flex w-full flex-row items-center justify-between px-6 py-4">
|
||||
<nav className="absolute top-0 z-20 flex w-full flex-row items-center justify-between px-6 py-4">
|
||||
<ThirdwebMiniLogo className="max-h-7" />
|
||||
<ColorModeToggle />
|
||||
</nav>
|
||||
|
||||
@@ -25,19 +25,19 @@ export function InviteSection(props: {
|
||||
}) {
|
||||
const teamPlan = getValidTeamPlan(props.team);
|
||||
let bottomSection: React.ReactNode = null;
|
||||
const inviteEnabled = teamPlan === "pro" && props.userHasEditPermission;
|
||||
const inviteEnabled = teamPlan !== "free" && props.userHasEditPermission;
|
||||
|
||||
if (teamPlan !== "pro") {
|
||||
if (teamPlan === "free") {
|
||||
bottomSection = (
|
||||
<div className="lg:px6 flex items-center justify-between gap-4 border-border border-t px-4 py-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
This feature is only available on the{" "}
|
||||
This feature is not available on the Free Plan.{" "}
|
||||
<Link
|
||||
href="https://thirdweb.com/pricing"
|
||||
target="_blank"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Pro plan <ExternalLinkIcon className="inline size-3" />
|
||||
View plans <ExternalLinkIcon className="inline size-3" />
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
|
||||
+71
-20
@@ -14,7 +14,9 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { EllipsisIcon, SearchIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type RoleFilterValue = "ALL ROLES" | TeamAccountRole;
|
||||
|
||||
export function ManageMembersSection(props: {
|
||||
team: Team;
|
||||
@@ -23,6 +25,36 @@ export function ManageMembersSection(props: {
|
||||
}) {
|
||||
let topSection: React.ReactNode = null;
|
||||
|
||||
const [role, setRole] = useState<RoleFilterValue>("ALL ROLES");
|
||||
const [sortBy, setSortBy] = useState<MemberSortId>("date");
|
||||
|
||||
const membersToShow = useMemo(() => {
|
||||
let value = props.members;
|
||||
if (role !== "ALL ROLES") {
|
||||
value = value.filter((m) => m.role === role);
|
||||
}
|
||||
|
||||
switch (sortBy) {
|
||||
case "date":
|
||||
value = value.sort(
|
||||
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
|
||||
);
|
||||
break;
|
||||
case "a-z":
|
||||
value = value.sort((a, b) =>
|
||||
a.account.name.localeCompare(b.account.name),
|
||||
);
|
||||
break;
|
||||
case "z-a":
|
||||
value = value.sort((a, b) =>
|
||||
b.account.name.localeCompare(a.account.name),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}, [role, props.members, sortBy]);
|
||||
|
||||
if (!props.userHasEditPermission) {
|
||||
topSection = (
|
||||
<div className="border-border border-b p-4">
|
||||
@@ -39,14 +71,14 @@ export function ManageMembersSection(props: {
|
||||
id="select-all"
|
||||
className="border-muted-foreground data-[state=checked]:border-inverted"
|
||||
disabled={
|
||||
!props.userHasEditPermission || props.members.length === 0
|
||||
!props.userHasEditPermission || membersToShow.length === 0
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="select-all"
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
>
|
||||
Select All ({props.members.length})
|
||||
Select All ({membersToShow.length})
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +86,7 @@ export function ManageMembersSection(props: {
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="!h-auto !w-auto p-1.5"
|
||||
disabled={!props.userHasEditPermission || props.members.length === 0}
|
||||
disabled={!props.userHasEditPermission || membersToShow.length === 0}
|
||||
>
|
||||
<EllipsisIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
@@ -68,7 +100,14 @@ export function ManageMembersSection(props: {
|
||||
|
||||
<div className="h-3" />
|
||||
|
||||
<FiltersSection disabled={props.members.length === 0} />
|
||||
<FiltersSection
|
||||
// don't use membersToShow here
|
||||
disabled={props.members.length === 0}
|
||||
role={role}
|
||||
setRole={setRole}
|
||||
setSortBy={setSortBy}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
@@ -77,11 +116,14 @@ export function ManageMembersSection(props: {
|
||||
{/* Top section */}
|
||||
{topSection}
|
||||
|
||||
{props.members.length > 0 && (
|
||||
{membersToShow.length > 0 && (
|
||||
<ul>
|
||||
{props.members.map((member) => {
|
||||
{membersToShow.map((member) => {
|
||||
return (
|
||||
<li key={member.accountId}>
|
||||
<li
|
||||
key={member.accountId}
|
||||
className="border-border border-b last:border-b-0"
|
||||
>
|
||||
<MemberRow
|
||||
member={member}
|
||||
userHasEditPermission={props.userHasEditPermission}
|
||||
@@ -93,7 +135,7 @@ export function ManageMembersSection(props: {
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{props.members.length === 0 && (
|
||||
{membersToShow.length === 0 && (
|
||||
<div className="flex justify-center px-4 py-10">
|
||||
<p className="text-muted-foreground text-sm">No Members Found</p>
|
||||
</div>
|
||||
@@ -108,7 +150,7 @@ function MemberRow(props: {
|
||||
userHasEditPermission: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-border border-b px-4 py-4">
|
||||
<div className="flex items-center justify-between px-4 py-4">
|
||||
<div className="flex items-center gap-3 lg:gap-4">
|
||||
{/* Checkbox */}
|
||||
<Checkbox
|
||||
@@ -150,7 +192,12 @@ function MemberRow(props: {
|
||||
|
||||
function FiltersSection(props: {
|
||||
disabled: boolean;
|
||||
role: RoleFilterValue;
|
||||
setRole: (role: RoleFilterValue) => void;
|
||||
setSortBy: (sortBy: MemberSortId) => void;
|
||||
sortBy: MemberSortId;
|
||||
}) {
|
||||
const { role, setRole, setSortBy, sortBy } = props;
|
||||
return (
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center">
|
||||
{/* Search */}
|
||||
@@ -164,8 +211,12 @@ function FiltersSection(props: {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 items-center gap-3 lg:flex">
|
||||
<RoleSelector disabled={props.disabled} />
|
||||
<SortMembersBy disabled={props.disabled} />
|
||||
<RoleSelector disabled={props.disabled} role={role} setRole={setRole} />
|
||||
<SortMembersBy
|
||||
disabled={props.disabled}
|
||||
setSortBy={setSortBy}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -175,8 +226,10 @@ type MemberSortId = "date" | "a-z" | "z-a";
|
||||
|
||||
function SortMembersBy(props: {
|
||||
disabled?: boolean;
|
||||
setSortBy: (sortBy: MemberSortId) => void;
|
||||
sortBy: MemberSortId;
|
||||
}) {
|
||||
const [sortBy, setSortBy] = useState<MemberSortId>("date");
|
||||
const { sortBy, setSortBy } = props;
|
||||
const valueToLabel: Record<MemberSortId, string> = {
|
||||
date: "Date",
|
||||
"a-z": "Name (A-Z)",
|
||||
@@ -211,19 +264,17 @@ function SortMembersBy(props: {
|
||||
|
||||
function RoleSelector(props: {
|
||||
disabled?: boolean;
|
||||
role: RoleFilterValue;
|
||||
setRole: (role: RoleFilterValue) => void;
|
||||
}) {
|
||||
const roles: (TeamAccountRole | "ALL ROLES")[] = [
|
||||
"OWNER",
|
||||
"MEMBER",
|
||||
"ALL ROLES",
|
||||
];
|
||||
const [role, setRole] = useState<TeamAccountRole | "ALL ROLES">("ALL ROLES");
|
||||
const { role, setRole } = props;
|
||||
const roles: RoleFilterValue[] = ["OWNER", "MEMBER", "ALL ROLES"];
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={role}
|
||||
onValueChange={(v) => {
|
||||
setRole(v as TeamAccountRole);
|
||||
setRole(v as RoleFilterValue);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
|
||||
+24
-42
@@ -1,7 +1,7 @@
|
||||
import type { Team } from "@/api/team";
|
||||
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 {
|
||||
BadgeContainer,
|
||||
mobileViewport,
|
||||
@@ -34,37 +34,22 @@ export const Mobile: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const freeTeam: Team = {
|
||||
id: "team-id-foo-bar",
|
||||
name: "Team XYZ",
|
||||
slug: "team-slug-foo-bar",
|
||||
createdAt: "2023-07-07T19:21:33.604Z",
|
||||
updatedAt: "2024-07-11T00:01:02.241Z",
|
||||
billingStatus: "validPayment",
|
||||
billingPlan: "free",
|
||||
billingEmail: "[email protected]",
|
||||
};
|
||||
const freeTeam = teamStub("foo", "free");
|
||||
const proTeam = teamStub("bar", "pro");
|
||||
const growthTeam = teamStub("bazz", "growth");
|
||||
|
||||
const proTeam: Team = {
|
||||
id: "team-id-foo-bar",
|
||||
name: "Team XYZ",
|
||||
slug: "team-slug-foo-bar",
|
||||
createdAt: "2023-07-07T19:21:33.604Z",
|
||||
updatedAt: "2024-07-11T00:01:02.241Z",
|
||||
billingStatus: "validPayment",
|
||||
billingPlan: "pro",
|
||||
billingEmail: "[email protected]",
|
||||
};
|
||||
|
||||
function createMemberStub(id: string, role: TeamAccountRole): TeamMember {
|
||||
function createMemberStub(
|
||||
id: string,
|
||||
role: TeamAccountRole,
|
||||
createdHours: number,
|
||||
): TeamMember {
|
||||
const date = new Date();
|
||||
// add random time to the date
|
||||
date.setHours(Math.floor(Math.random() * 24));
|
||||
date.setHours(createdHours);
|
||||
|
||||
const member: TeamMember = {
|
||||
account: {
|
||||
email: `user-${id}@foo.com`,
|
||||
name: `username-${id}`,
|
||||
name: id,
|
||||
},
|
||||
accountId: `account-id-${id}`,
|
||||
createdAt: date,
|
||||
@@ -78,9 +63,9 @@ function createMemberStub(id: string, role: TeamAccountRole): TeamMember {
|
||||
}
|
||||
|
||||
const membersStub: TeamMember[] = [
|
||||
createMemberStub("1", "OWNER"),
|
||||
createMemberStub("2", "MEMBER"),
|
||||
createMemberStub("3", "OWNER"),
|
||||
createMemberStub("first-member", "OWNER", 1),
|
||||
createMemberStub("third-member", "MEMBER", 3),
|
||||
createMemberStub("second-member", "OWNER", 2),
|
||||
];
|
||||
|
||||
function Story() {
|
||||
@@ -107,7 +92,7 @@ function CompVariants() {
|
||||
|
||||
{/* Invite */}
|
||||
<div className="flex flex-col gap-10">
|
||||
<BadgeContainer label="Not a Pro Team">
|
||||
<BadgeContainer label="Free Team">
|
||||
<InviteSection team={freeTeam} userHasEditPermission={false} />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -118,35 +103,32 @@ function CompVariants() {
|
||||
<BadgeContainer label="Pro, User has permission">
|
||||
<InviteSection team={proTeam} userHasEditPermission={true} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Growth, User has permission">
|
||||
<InviteSection team={growthTeam} userHasEditPermission={true} />
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
<div className="my-10" />
|
||||
|
||||
{/* Invite */}
|
||||
<h2 className="py-4 font-semibold text-3xl">Team Members Variants</h2>
|
||||
|
||||
<div className="flex flex-col gap-10">
|
||||
<BadgeContainer label="Pro Team, has permission">
|
||||
<BadgeContainer label="Has permission">
|
||||
<ManageMembersSection
|
||||
team={proTeam}
|
||||
team={freeTeam}
|
||||
userHasEditPermission={true}
|
||||
members={membersStub}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Not a Pro Team, No permission">
|
||||
<BadgeContainer label="No permission">
|
||||
<ManageMembersSection
|
||||
team={freeTeam}
|
||||
userHasEditPermission={false}
|
||||
members={membersStub}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Pro Team, No permission">
|
||||
<ManageMembersSection
|
||||
team={proTeam}
|
||||
userHasEditPermission={false}
|
||||
members={membersStub}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+13
@@ -1,5 +1,7 @@
|
||||
import type { Team } from "@/api/team";
|
||||
import type { TeamMember } from "@/api/team-members";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { AlertCircleIcon } from "lucide-react";
|
||||
import { InviteSection } from "./InviteSection";
|
||||
import { ManageMembersSection } from "./ManageMembersSection";
|
||||
|
||||
@@ -10,6 +12,17 @@ export function TeamMembersSettingsPage(props: {
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Alert variant="info">
|
||||
<AlertCircleIcon className="size-5 text-red-400" />
|
||||
<AlertTitle>
|
||||
Inviting and Managing Team Members is not available yet
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
This feature will be available in Q4 2024
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="h-10" />
|
||||
|
||||
<InviteSection
|
||||
team={props.team}
|
||||
userHasEditPermission={props.userHasEditPermission}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { getMembers } from "@/api/team-members";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAccount } from "../../../../../../account/settings/getAccount";
|
||||
import { TeamMembersSettingsPage } from "./TeamMembersSettingsPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -8,12 +9,19 @@ export default async function Page(props: {
|
||||
team_slug: string;
|
||||
};
|
||||
}) {
|
||||
const [team, members] = await Promise.all([
|
||||
const [account, team, members] = await Promise.all([
|
||||
getAccount(),
|
||||
getTeamBySlug(props.params.team_slug),
|
||||
getMembers(props.params.team_slug),
|
||||
]);
|
||||
|
||||
if (!team) {
|
||||
if (!team || !account || !members) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const accountMemberInfo = members.find((m) => m.accountId === account.id);
|
||||
|
||||
if (!accountMemberInfo) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -21,8 +29,7 @@ export default async function Page(props: {
|
||||
<TeamMembersSettingsPage
|
||||
team={team}
|
||||
members={members}
|
||||
// TODO
|
||||
userHasEditPermission={true}
|
||||
userHasEditPermission={accountMemberInfo.role === "OWNER"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const SettingsUsagePage = () => {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="mb-2 font-semibold text-3xl tracking-tight">Usage</h1>
|
||||
<div className="flex flex-col items-start justify-between lg:flex-row lg:items-center">
|
||||
<div className="flex flex-col items-start justify-between gap-2 lg:flex-row lg:items-center">
|
||||
<BillingPlan account={account} />
|
||||
<BillingPeriod account={account} usage={usageQuery.data} />
|
||||
</div>
|
||||
|
||||
+177
-18
@@ -1,42 +1,201 @@
|
||||
"use client";
|
||||
import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
useWalletUsageAggregate,
|
||||
useWalletUsagePeriod,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useMemo } from "react";
|
||||
import { differenceInDays, format, subDays } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { ConnectAnalyticsDashboardUI } from "./ConnectAnalyticsDashboardUI";
|
||||
|
||||
export function ConnectAnalyticsDashboard(props: {
|
||||
clientId: string;
|
||||
}) {
|
||||
const allTimeFrom = useMemo(
|
||||
() => new Date(Date.now() - 135 * 24 * 60 * 60 * 1000),
|
||||
[],
|
||||
const [range, setRange] = useState<Range>(() =>
|
||||
getLastNDaysRange("last-120"),
|
||||
);
|
||||
const from = useMemo(
|
||||
() => new Date(Date.now() - 135 * 24 * 60 * 60 * 1000),
|
||||
[],
|
||||
|
||||
// use date-fns to calculate the number of days in the range
|
||||
const daysInRange = differenceInDays(range.to, range.from);
|
||||
|
||||
const [intervalType, setIntervalType] = useState<"day" | "week">(
|
||||
daysInRange > 30 ? "week" : "day",
|
||||
);
|
||||
const to = useMemo(() => new Date(), []);
|
||||
|
||||
const walletUsageQuery = useWalletUsagePeriod({
|
||||
clientId: props.clientId,
|
||||
from,
|
||||
to,
|
||||
period: "day",
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: intervalType,
|
||||
});
|
||||
|
||||
const walletUsageAggregateQuery = useWalletUsageAggregate({
|
||||
clientId: props.clientId,
|
||||
from: allTimeFrom,
|
||||
to,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
});
|
||||
|
||||
return (
|
||||
<ConnectAnalyticsDashboardUI
|
||||
walletUsage={walletUsageQuery.data || []}
|
||||
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
|
||||
isPending={
|
||||
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
|
||||
<div>
|
||||
<div className="flex gap-3">
|
||||
<DateRangeSelector
|
||||
range={range}
|
||||
setRange={(newRange) => {
|
||||
setRange(newRange);
|
||||
const days = differenceInDays(newRange.to, newRange.from);
|
||||
setIntervalType(days > 30 ? "week" : "day");
|
||||
}}
|
||||
/>
|
||||
<IntervalSelector
|
||||
intervalType={intervalType}
|
||||
setIntervalType={setIntervalType}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-4" />
|
||||
<ConnectAnalyticsDashboardUI
|
||||
walletUsage={walletUsageQuery.data || []}
|
||||
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
|
||||
isPending={
|
||||
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const durationPresets = [
|
||||
{
|
||||
name: "Last 7 Days",
|
||||
id: "last-7",
|
||||
days: 7,
|
||||
},
|
||||
{
|
||||
name: "Last 30 Days",
|
||||
id: "last-30",
|
||||
days: 30,
|
||||
},
|
||||
{
|
||||
name: "Last 60 Days",
|
||||
id: "last-60",
|
||||
days: 60,
|
||||
},
|
||||
{
|
||||
name: "Last 120 Days",
|
||||
id: "last-120",
|
||||
days: 120,
|
||||
},
|
||||
] as const;
|
||||
|
||||
type DurationId = (typeof durationPresets)[number]["id"];
|
||||
|
||||
type Range = {
|
||||
type: DurationId | "custom";
|
||||
label?: string;
|
||||
from: Date;
|
||||
to: Date;
|
||||
};
|
||||
|
||||
function getLastNDaysRange(id: DurationId) {
|
||||
const durationInfo = durationPresets.find((preset) => preset.id === id);
|
||||
if (!durationInfo) {
|
||||
throw new Error("Invalid duration id");
|
||||
}
|
||||
|
||||
const todayDate = new Date();
|
||||
|
||||
const value: Range = {
|
||||
type: id,
|
||||
from: subDays(todayDate, durationInfo.days),
|
||||
to: todayDate,
|
||||
label: durationInfo.name,
|
||||
};
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function DateRangeSelector(props: {
|
||||
range: Range;
|
||||
setRange: (range: Range) => void;
|
||||
}) {
|
||||
const { range, setRange } = props;
|
||||
|
||||
return (
|
||||
<DatePickerWithRange
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
setFrom={(from) =>
|
||||
setRange({
|
||||
from,
|
||||
to: range.to,
|
||||
type: "custom",
|
||||
})
|
||||
}
|
||||
setTo={(to) =>
|
||||
setRange({
|
||||
from: range.from,
|
||||
to,
|
||||
type: "custom",
|
||||
})
|
||||
}
|
||||
header={
|
||||
<div className="mb-2 border-border border-b p-4">
|
||||
<Select
|
||||
value={range.type}
|
||||
onValueChange={(id: DurationId) => {
|
||||
setRange(getLastNDaysRange(id));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex bg-transparent">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
{durationPresets.map((preset) => (
|
||||
<SelectItem key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
{range.type === "custom" && (
|
||||
<SelectItem value="custom">
|
||||
{format(range.from, "LLL dd, y")} -{" "}
|
||||
{format(range.to, "LLL dd, y")}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
labelOverride={range.label}
|
||||
className="w-auto"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function IntervalSelector(props: {
|
||||
intervalType: "day" | "week";
|
||||
setIntervalType: (intervalType: "day" | "week") => void;
|
||||
}) {
|
||||
return (
|
||||
<Select
|
||||
value={props.intervalType}
|
||||
onValueChange={(value: "day" | "week") => {
|
||||
props.setIntervalType(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-auto hover:bg-muted">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="week"> Weekly </SelectItem>
|
||||
<SelectItem value="day"> Daily</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container py-8">
|
||||
<div className="container max-w-[1150px] py-8">
|
||||
<ConnectAnalyticsDashboardUI
|
||||
walletUsage={createWalletStatsStub(30)}
|
||||
aggregateWalletUsage={createWalletStatsStub(30)}
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex flex-col gap-6 py-8">
|
||||
<div className="container flex max-w-[1150px] flex-col gap-6 py-8">
|
||||
<BadgeContainer label="30 days">
|
||||
<DailyConnectionsChartCard
|
||||
walletStats={createWalletStatsStub(30)}
|
||||
|
||||
+44
-23
@@ -36,6 +36,8 @@ export function WalletConnectorsChartCard(props: {
|
||||
walletStats: WalletStats[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
// show top 10 wallets as distinct, and combine the rest as "Others"
|
||||
const topWalletsToShow = 10;
|
||||
const { walletStats } = props;
|
||||
const [chartToShow, setChartToShow] = useState<ChartToShow>(
|
||||
"uniqueWalletsConnected",
|
||||
@@ -48,40 +50,59 @@ export function WalletConnectorsChartCard(props: {
|
||||
const { chartConfig, chartData } = useMemo(() => {
|
||||
const _chartConfig: ChartConfig = {};
|
||||
const _chartDataMap: Map<string, ChartData> = new Map();
|
||||
|
||||
for (const data of walletStats) {
|
||||
const chartData = _chartDataMap.get(data.date);
|
||||
const dataKey = data.walletType;
|
||||
const walletTypeToValueMap: Map<string, number> = new Map();
|
||||
// for each stat, add it in _chartDataMap
|
||||
for (const stat of walletStats) {
|
||||
const chartData = _chartDataMap.get(stat.date);
|
||||
const { walletType } = stat;
|
||||
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData) {
|
||||
_chartDataMap.set(data.date, {
|
||||
time: format(new Date(data.date), "MMM dd"),
|
||||
[data.walletType]: data[chartToShow],
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
[walletType]: stat[chartToShow],
|
||||
} as ChartData);
|
||||
} else {
|
||||
if (dataKey in chartData) {
|
||||
chartData[dataKey] += data[chartToShow];
|
||||
} else {
|
||||
chartData[dataKey] = data[chartToShow];
|
||||
chartData[walletType] =
|
||||
(chartData[walletType] || 0) + stat[chartToShow];
|
||||
}
|
||||
|
||||
walletTypeToValueMap.set(
|
||||
walletType,
|
||||
stat[chartToShow] + (walletTypeToValueMap.get(walletType) || 0),
|
||||
);
|
||||
}
|
||||
|
||||
const walletTypesSorted = Array.from(walletTypeToValueMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((w) => w[0]);
|
||||
|
||||
const walletTypesToShow = walletTypesSorted.slice(0, topWalletsToShow);
|
||||
const walletTypesToTagAsOthers = walletTypesSorted.slice(topWalletsToShow);
|
||||
|
||||
// replace walletTypesToTagAsOthers walletType with "other"
|
||||
for (const data of _chartDataMap.values()) {
|
||||
for (const walletType in data) {
|
||||
if (walletTypesToTagAsOthers.includes(walletType)) {
|
||||
data.others = (data.others || 0) + (data[walletType] || 0);
|
||||
delete data[walletType];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create chart config for each wallet type and assign a unique color, start from 0hue to 360hue
|
||||
const uniqueWalletTypes = Array.from(
|
||||
new Set(walletStats.map((data) => data.walletType)),
|
||||
);
|
||||
const hueIncrement = 360 / uniqueWalletTypes.length;
|
||||
|
||||
for (let i = 0; i < uniqueWalletTypes.length; i++) {
|
||||
const walletType = uniqueWalletTypes[i];
|
||||
|
||||
walletTypesToShow.forEach((walletType, i) => {
|
||||
_chartConfig[walletType] = {
|
||||
label: uniqueWalletTypes[i],
|
||||
color: `hsl(${i + hueIncrement * i}deg, var(--chart-saturation), var(--chart-lightness))`,
|
||||
label: walletTypesToShow[i],
|
||||
color: `hsl(var(--chart-${(i % 10) + 1}))`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Add Other
|
||||
walletTypesToShow.push("others");
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
|
||||
return {
|
||||
chartData: Array.from(_chartDataMap.values()),
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="30 days">
|
||||
<WalletConnectorsChartCard
|
||||
walletStats={createWalletStatsStub(30)}
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="30 days">
|
||||
<WalletDistributionChartCard
|
||||
walletStats={createWalletStatsStub(30)}
|
||||
|
||||
+66
-63
@@ -21,90 +21,104 @@ import { useMemo, useState } from "react";
|
||||
import { Pie, PieChart } from "recharts";
|
||||
import { EmptyChartState, LoadingChartState } from "./EmptyChartState";
|
||||
|
||||
type ChartToShow = "unique" | "total";
|
||||
type ChartToShow = "totalConnections" | "uniqueWalletsConnected";
|
||||
|
||||
type ChartData = {
|
||||
walletType: string;
|
||||
totalWallets: number;
|
||||
uniqueWallets: number;
|
||||
value: number;
|
||||
fill: string;
|
||||
};
|
||||
|
||||
const chartLabelToShow: Record<ChartToShow, string> = {
|
||||
unique: "Unique Wallets",
|
||||
total: "Total Wallets",
|
||||
uniqueWalletsConnected: "Unique Wallets",
|
||||
totalConnections: "Total Wallets",
|
||||
};
|
||||
|
||||
export function WalletDistributionChartCard(props: {
|
||||
walletStats: WalletStats[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
// show these many top wallets as distinct, and combine the rest as "Others"
|
||||
const topWalletsToShow = 10;
|
||||
const { walletStats } = props;
|
||||
const [chartToShow, setChartToShow] = useState<ChartToShow>("total");
|
||||
const chartToShowOptions: ChartToShow[] = ["total", "unique"];
|
||||
const [chartToShow, setChartToShow] =
|
||||
useState<ChartToShow>("totalConnections");
|
||||
const chartToShowOptions: ChartToShow[] = [
|
||||
"totalConnections",
|
||||
"uniqueWalletsConnected",
|
||||
];
|
||||
|
||||
const { chartConfig, chartData, totalConnections, uniqueConnections } =
|
||||
useMemo(() => {
|
||||
const _chartConfig: ChartConfig = {};
|
||||
const _chartDataMap: Map<
|
||||
string,
|
||||
{
|
||||
total: number;
|
||||
unique: number;
|
||||
}
|
||||
> = new Map();
|
||||
const _chartDataMap: Map<string, number> = new Map();
|
||||
const walletTypeToValueMap: Map<string, number> = new Map();
|
||||
|
||||
let _totalConnections = 0;
|
||||
let _uniqueConnections = 0;
|
||||
|
||||
for (const data of walletStats) {
|
||||
const chartData = _chartDataMap.get(data.walletType);
|
||||
for (const stat of walletStats) {
|
||||
const { walletType } = stat;
|
||||
const chartData = _chartDataMap.get(walletType);
|
||||
|
||||
_totalConnections += data.totalConnections;
|
||||
_uniqueConnections += data.uniqueWalletsConnected;
|
||||
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData) {
|
||||
_chartDataMap.set(data.walletType, {
|
||||
total: data.totalConnections,
|
||||
unique: data.uniqueWalletsConnected,
|
||||
});
|
||||
} else {
|
||||
_chartDataMap.set(data.walletType, {
|
||||
total: chartData.total + data.totalConnections,
|
||||
unique: chartData.unique + data.uniqueWalletsConnected,
|
||||
});
|
||||
}
|
||||
_totalConnections += stat.totalConnections;
|
||||
_uniqueConnections += stat.uniqueWalletsConnected;
|
||||
_chartDataMap.set(walletType, (chartData || 0) + stat[chartToShow]);
|
||||
walletTypeToValueMap.set(
|
||||
walletType,
|
||||
(walletTypeToValueMap.get(walletType) || 0) + stat[chartToShow],
|
||||
);
|
||||
}
|
||||
|
||||
// create chart config for each wallet type and assign a unique color, start from 0hue to 360hue
|
||||
const uniqueWalletTypes = Array.from(
|
||||
new Set(walletStats.map((data) => data.walletType)),
|
||||
const walletTypesSortedByValue = Array.from(
|
||||
walletTypeToValueMap.entries(),
|
||||
)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((w) => w[0]);
|
||||
|
||||
const walletTypesToShow = walletTypesSortedByValue.slice(
|
||||
0,
|
||||
topWalletsToShow,
|
||||
);
|
||||
|
||||
const hueIncrement = 360 / uniqueWalletTypes.length;
|
||||
const walletTypesToTagAsOthers =
|
||||
walletTypesSortedByValue.slice(topWalletsToShow);
|
||||
|
||||
for (let i = 0; i < uniqueWalletTypes.length; i++) {
|
||||
const walletType = uniqueWalletTypes[i];
|
||||
_chartConfig[walletType] = {
|
||||
label: uniqueWalletTypes[i],
|
||||
color: `hsl(${i + hueIncrement * i}deg, var(--chart-saturation), var(--chart-lightness))`,
|
||||
};
|
||||
for (const walletType of walletTypesToTagAsOthers) {
|
||||
const val = _chartDataMap.get(walletType);
|
||||
if (val) {
|
||||
const othersVal = _chartDataMap.get("others");
|
||||
_chartDataMap.set("others", othersVal ? othersVal + val : val);
|
||||
}
|
||||
|
||||
_chartDataMap.delete(walletType);
|
||||
}
|
||||
|
||||
const _chartConfig: ChartConfig = {};
|
||||
walletTypesToShow.forEach((walletType, i) => {
|
||||
_chartConfig[walletType] = {
|
||||
label: walletTypesToShow[i],
|
||||
color: `hsl(var(--chart-${(i % 10) + 1}))`,
|
||||
};
|
||||
});
|
||||
|
||||
// Add Others
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
|
||||
const _chartData: ChartData[] = Array.from(_chartDataMap).map(
|
||||
([walletType, data]) => {
|
||||
return {
|
||||
walletType,
|
||||
totalWallets: data.total,
|
||||
uniqueWallets: data.unique,
|
||||
value: data,
|
||||
fill: _chartConfig[walletType].color || "transparent",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// sort the data
|
||||
_chartData.sort((a, b) => b.totalWallets - a.totalWallets);
|
||||
_chartData.sort((a, b) => b.value - a.value);
|
||||
|
||||
return {
|
||||
chartData: _chartData,
|
||||
@@ -112,7 +126,7 @@ export function WalletDistributionChartCard(props: {
|
||||
totalConnections: _totalConnections,
|
||||
uniqueConnections: _uniqueConnections,
|
||||
};
|
||||
}, [walletStats]);
|
||||
}, [walletStats, chartToShow]);
|
||||
|
||||
const disableActions = props.isPending || chartData.length === 0;
|
||||
return (
|
||||
@@ -152,23 +166,14 @@ export function WalletDistributionChartCard(props: {
|
||||
getData={async () => {
|
||||
const header = [
|
||||
"Wallet",
|
||||
"Total Connections",
|
||||
"Unique Connections",
|
||||
"Percentage of Total connections",
|
||||
"Percentage of Unique connections",
|
||||
`${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
|
||||
`Percentage of ${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
|
||||
];
|
||||
const rows = chartData.map((d) => {
|
||||
return [
|
||||
// name
|
||||
d.walletType,
|
||||
// total connections
|
||||
d.totalWallets.toString(),
|
||||
// unique connections
|
||||
d.uniqueWallets.toString(),
|
||||
// percentage of total connections
|
||||
`${((d.totalWallets / totalConnections) * 100).toFixed(2)}%`,
|
||||
// percentage of unique connections
|
||||
`${((d.uniqueWallets / uniqueConnections) * 100).toFixed(2)}%`,
|
||||
d.value.toString(),
|
||||
`${((d.value / (chartToShow === "totalConnections" ? totalConnections : uniqueConnections)) * 100).toFixed(2)}%`,
|
||||
];
|
||||
});
|
||||
return {
|
||||
@@ -197,7 +202,7 @@ export function WalletDistributionChartCard(props: {
|
||||
valueFormatter={(v) => {
|
||||
if (typeof v === "number") {
|
||||
const sumValue =
|
||||
chartToShow === "unique"
|
||||
chartToShow === "uniqueWalletsConnected"
|
||||
? uniqueConnections
|
||||
: totalConnections;
|
||||
const percentageValue = ((v / sumValue) * 100).toFixed(2);
|
||||
@@ -210,9 +215,7 @@ export function WalletDistributionChartCard(props: {
|
||||
<ChartLegend content={<ChartLegendContent />} className="" />
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey={
|
||||
chartToShow === "unique" ? "uniqueWallets" : "totalWallets"
|
||||
}
|
||||
dataKey="value"
|
||||
nameKey="walletType"
|
||||
innerRadius={60}
|
||||
strokeWidth={2}
|
||||
|
||||
+8
@@ -13,6 +13,14 @@ const walletsToPickFrom: WalletId[] = [
|
||||
"com.exodus",
|
||||
"app.phantom",
|
||||
"com.okex.wallet",
|
||||
"io.clingon",
|
||||
"com.broearn",
|
||||
"com.coinomi",
|
||||
"com.ripio",
|
||||
"com.sabay.wallet",
|
||||
"io.tokoin",
|
||||
"world.fncy",
|
||||
"io.copiosa",
|
||||
];
|
||||
|
||||
const pickRandomWallet = () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DashboardTypeCookieSetter } from "@/components/DashboardTypeCookieSetter";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { TeamsUIBanner } from "../../components/DashboardTypeBanner";
|
||||
import { TWAutoConnect } from "../../components/autoconnect";
|
||||
|
||||
export default function RootTeamLayout(props: {
|
||||
@@ -9,7 +8,6 @@ export default function RootTeamLayout(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<TeamsUIBanner />
|
||||
<div className="flex grow flex-col">{props.children}</div>
|
||||
<TWAutoConnect />
|
||||
<AppFooter />
|
||||
|
||||
+2
-3
@@ -23,11 +23,10 @@ import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { replaceTemplateValues } from "lib/deployment/template-values";
|
||||
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { CircleAlertIcon, ExternalLinkIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { FormProvider, type UseFormReturn, useForm } from "react-hook-form";
|
||||
import { FiHelpCircle } from "react-icons/fi";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import {
|
||||
@@ -856,7 +855,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
<span className="tex-sm">
|
||||
Deploy at a deterministic address
|
||||
</span>
|
||||
<FiHelpCircle className="size-4" />
|
||||
<InfoIcon className="size-4" />
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
</CheckboxWithLabel>
|
||||
|
||||
@@ -14,9 +14,9 @@ import { ContractFunctionsOverview } from "components/contract-functions/contrac
|
||||
import { format } from "date-fns/format";
|
||||
import { correctAndUniqueLicenses } from "lib/licenses";
|
||||
import { replaceIpfsUrl } from "lib/sdk";
|
||||
import { ShieldCheckIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { BiPencil } from "react-icons/bi";
|
||||
import { BsShieldCheck } from "react-icons/bs";
|
||||
import { VscBook, VscCalendar, VscServer } from "react-icons/vsc";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
@@ -181,7 +181,7 @@ export const PublishedContract: React.FC<PublishedContractProps> = ({
|
||||
{publishedContract?.audit && (
|
||||
<ListItem>
|
||||
<Flex gap={2} alignItems="flex-start">
|
||||
<Icon as={BsShieldCheck} boxSize={5} color="green" />
|
||||
<ShieldCheckIcon className="size-5 text-green-500" />
|
||||
<Flex direction="column" gap={1}>
|
||||
<Heading as="h5" size="label.sm">
|
||||
Audit Report
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ButtonGroup, type ButtonGroupProps, Icon } from "@chakra-ui/react";
|
||||
import { ButtonGroup, type ButtonGroupProps } from "@chakra-ui/react";
|
||||
import { DiscordIcon } from "components/icons/brand-icons/DiscordIcon";
|
||||
import { GithubIcon } from "components/icons/brand-icons/GithubIcon";
|
||||
import { LinkedInIcon } from "components/icons/brand-icons/LinkedinIcon";
|
||||
@@ -8,7 +8,7 @@ import { RedditIcon } from "components/icons/brand-icons/RedditIcon";
|
||||
import { TelegramIcon } from "components/icons/brand-icons/TelegramIcon";
|
||||
import { XIcon } from "components/icons/brand-icons/XIcon";
|
||||
import type { ProfileMetadata } from "constants/schemas";
|
||||
import { FiGlobe } from "react-icons/fi";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import { LinkButton, TrackedIconButton } from "tw-components";
|
||||
import { hostnameEndsWith } from "../../../utils/url";
|
||||
|
||||
@@ -80,7 +80,7 @@ export const PublisherSocials: React.FC<PublisherSocialsProps> = ({
|
||||
href={publisherProfile.website}
|
||||
bg="transparent"
|
||||
aria-label="website"
|
||||
icon={<Icon as={FiGlobe} />}
|
||||
icon={<GlobeIcon className="size-4" />}
|
||||
category={TRACKING_CATEGORY}
|
||||
label="website"
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Flex, Icon } from "@chakra-ui/react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { WandIcon } from "lucide-react";
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { SearchIcon, WandIcon } from "lucide-react";
|
||||
import { Heading, LinkButton } from "tw-components";
|
||||
|
||||
interface HackathonFooterProps {
|
||||
@@ -81,7 +80,7 @@ export const HackathonFooter = ({
|
||||
h="68px"
|
||||
w={{ base: "90%", md: 80 }}
|
||||
fontSize="20px"
|
||||
leftIcon={<Icon as={FiSearch} />}
|
||||
leftIcon={<SearchIcon className="size-5" />}
|
||||
color="black"
|
||||
flexShrink={0}
|
||||
background="rgba(255,255,255,1)"
|
||||
|
||||
@@ -3,16 +3,14 @@ import {
|
||||
Code,
|
||||
type CodeProps,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButton,
|
||||
useColorModeValue,
|
||||
useTheme,
|
||||
} from "@chakra-ui/react";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { CheckIcon, CopyIcon, ZapIcon } from "lucide-react";
|
||||
import { Highlight, themes } from "prism-react-renderer";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { BsLightning } from "react-icons/bs";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { Text } from "tw-components";
|
||||
|
||||
@@ -133,7 +131,7 @@ export const HomePageCodeBlock: React.FC<CodeBlockProps> = ({
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
icon={<Icon as={BsLightning} />}
|
||||
icon={<ZapIcon className="size-4" />}
|
||||
/>
|
||||
) : (
|
||||
<Box w="32px" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Flex, Icon, SimpleGrid } from "@chakra-ui/react";
|
||||
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { HomepageSection } from "components/product-pages/homepage/HomepageSection";
|
||||
import { ZapIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsFillLightningChargeFill } from "react-icons/bs";
|
||||
import { Heading, Text, TrackedLink, TrackedLinkButton } from "tw-components";
|
||||
import { getCookie } from "../../../stores/SyncStoreToCookies";
|
||||
import { Aurora } from "../Aurora";
|
||||
@@ -153,7 +153,7 @@ function GetStartedButtonLink(props: {
|
||||
|
||||
return (
|
||||
<TrackedLinkButton
|
||||
leftIcon={<Icon as={BsFillLightningChargeFill} boxSize={4} />}
|
||||
leftIcon={<ZapIcon className="size-4 fill-black" />}
|
||||
py={6}
|
||||
px={8}
|
||||
w="full"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Container, Flex, Icon, SimpleGrid } from "@chakra-ui/react";
|
||||
import { Container, Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { LandingDesktopMobileImage } from "components/landing-pages/desktop-mobile-image";
|
||||
import { ZapIcon } from "lucide-react";
|
||||
import type { StaticImageData } from "next/image";
|
||||
import type { ReactNode } from "react";
|
||||
import { BsFillLightningChargeFill } from "react-icons/bs";
|
||||
import { Heading, Text, TrackedLinkButton } from "tw-components";
|
||||
|
||||
interface HomePageCardProps {
|
||||
@@ -96,7 +96,7 @@ const HomePageCard = ({
|
||||
label={label}
|
||||
fontWeight="bold"
|
||||
maxW="190px"
|
||||
leftIcon={<Icon as={BsFillLightningChargeFill} boxSize={4} />}
|
||||
leftIcon={<ZapIcon className="size-4 fill-black" />}
|
||||
>
|
||||
{ctaText}
|
||||
</TrackedLinkButton>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||
import { ChevronRightIcon, XIcon } from "lucide-react";
|
||||
|
||||
export function AnnouncementBanner(props: {
|
||||
function AnnouncementBanner(props: {
|
||||
href: string;
|
||||
label: string;
|
||||
trackingLabel: string;
|
||||
@@ -29,9 +29,9 @@ export function AnnouncementBanner(props: {
|
||||
category="announcement"
|
||||
label={props.trackingLabel}
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
className="container flex cursor-pointer items-center gap-2 lg:justify-center"
|
||||
className="!text-white container flex cursor-pointer items-center gap-2 lg:justify-center "
|
||||
>
|
||||
<span className="inline-block font-semibold text-white leading-normal hover:underline">
|
||||
<span className="inline-block font-semibold leading-normal hover:underline">
|
||||
{props.label}
|
||||
</span>
|
||||
<ChevronRightIcon className="hidden size-5 opacity-80 lg:block" />
|
||||
@@ -49,3 +49,13 @@ export function AnnouncementBanner(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UnlimitedWalletsBanner() {
|
||||
return (
|
||||
<AnnouncementBanner
|
||||
href="/dashboard/settings/billing?coupon=FREEWALLETS"
|
||||
label='Claim 12 months of free in-app wallets. Use code "FREEWALLETS". Redeem offer by October 31st!'
|
||||
trackingLabel="unlimited-wallets"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ export const OnboardingSteps: React.FC<OnboardingStepsProps> = ({
|
||||
>
|
||||
<Heading size="title.sm">{title}</Heading>
|
||||
<Text>{description}</Text>
|
||||
<div className="mt-4 flex flex-row items-center">
|
||||
<div className="mt-4 flex flex-row items-center gap-2">
|
||||
{isLoggedIn ? (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { format } from "date-fns";
|
||||
import { format, subDays } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { PayCustomersTable } from "./components/PayCustomersTable";
|
||||
import { PayNewCustomers } from "./components/PayNewCustomers";
|
||||
@@ -125,19 +125,15 @@ export function PayAnalytics(props: { clientId: string }) {
|
||||
}
|
||||
|
||||
function getLastNDaysRange(id: DurationId) {
|
||||
const todayDate = new Date();
|
||||
const pastDate = new Date(todayDate);
|
||||
|
||||
const durationInfo = durationPresets.find((preset) => preset.id === id);
|
||||
if (!durationInfo) {
|
||||
throw new Error("Invalid duration id");
|
||||
}
|
||||
|
||||
pastDate.setDate(todayDate.getDate() - durationInfo.days);
|
||||
|
||||
const todayDate = new Date();
|
||||
const value: Range = {
|
||||
type: id,
|
||||
from: pastDate,
|
||||
from: subDays(todayDate, durationInfo.days),
|
||||
to: todayDate,
|
||||
label: durationInfo.name,
|
||||
};
|
||||
@@ -148,35 +144,6 @@ function getLastNDaysRange(id: DurationId) {
|
||||
function Filters(props: { range: Range; setRange: (range: Range) => void }) {
|
||||
const { range, setRange } = props;
|
||||
|
||||
const presets = (
|
||||
<div className="mb-2 border-border border-b p-4">
|
||||
<Select
|
||||
value={range.type}
|
||||
onValueChange={(id: DurationId) => {
|
||||
setRange(getLastNDaysRange(id));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex bg-transparent">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
{durationPresets.map((preset) => (
|
||||
<SelectItem key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
{range.type === "custom" && (
|
||||
<SelectItem value="custom">
|
||||
{format(range.from, "LLL dd, y")} -{" "}
|
||||
{format(range.to, "LLL dd, y")}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<DatePickerWithRange
|
||||
@@ -196,7 +163,34 @@ function Filters(props: { range: Range; setRange: (range: Range) => void }) {
|
||||
type: "custom",
|
||||
})
|
||||
}
|
||||
header={presets}
|
||||
header={
|
||||
<div className="mb-2 border-border border-b p-4">
|
||||
<Select
|
||||
value={range.type}
|
||||
onValueChange={(id: DurationId) => {
|
||||
setRange(getLastNDaysRange(id));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex bg-transparent">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
{durationPresets.map((preset) => (
|
||||
<SelectItem key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
{range.type === "custom" && (
|
||||
<SelectItem value="custom">
|
||||
{format(range.from, "LLL dd, y")} -{" "}
|
||||
{format(range.to, "LLL dd, y")}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
labelOverride={range.label}
|
||||
className="w-auto border-none p-0"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChakraNextImage } from "components/Image";
|
||||
import { FiMenu } from "react-icons/fi";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
import { Drawer, Heading, TrackedLink, TrackedLinkButton } from "tw-components";
|
||||
import {
|
||||
DEVELOPER_RESOURCES,
|
||||
@@ -35,7 +35,7 @@ export const MobileMenu: React.FC<FlexProps> = (props) => {
|
||||
</TrackedLinkButton>
|
||||
<IconButton
|
||||
aria-label="Homepage Menu"
|
||||
icon={<FiMenu />}
|
||||
icon={<MenuIcon className="size-4" />}
|
||||
variant="ghost"
|
||||
onClick={disclosure.onOpen}
|
||||
/>
|
||||
|
||||
@@ -220,7 +220,7 @@ export const AccountForm: React.FC<AccountFormProps> = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-row",
|
||||
"flex w-full flex-row items-center gap-2",
|
||||
showBillingButton ? "justify-between" : "justify-end",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -54,6 +54,14 @@ function statusStub(status: number) {
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="Prefill code - XYZ, Success - 200">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(200)}
|
||||
onCouponApplied={undefined}
|
||||
prefillPromoCode="XYZ"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Success - 200">
|
||||
<ApplyCouponCardUI
|
||||
submit={statusStub(200)}
|
||||
|
||||
@@ -16,7 +16,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
import { TagIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -37,9 +38,13 @@ function ApplyCouponCard(props: {
|
||||
teamId: string | undefined;
|
||||
onCouponApplied: (data: ActiveCouponResponse) => void;
|
||||
}) {
|
||||
const searchParams = useSearchParams();
|
||||
const couponCode = searchParams?.get("coupon");
|
||||
return (
|
||||
<ApplyCouponCardUI
|
||||
onCouponApplied={props.onCouponApplied}
|
||||
prefillPromoCode={couponCode || undefined}
|
||||
scrollIntoView={!!couponCode}
|
||||
submit={async (promoCode: string) => {
|
||||
const res = await fetch("/api/server-proxy/api/v1/coupons/redeem", {
|
||||
method: "POST",
|
||||
@@ -79,14 +84,30 @@ export function ApplyCouponCardUI(props: {
|
||||
data: null | ActiveCouponResponse;
|
||||
}>;
|
||||
onCouponApplied: ((data: ActiveCouponResponse) => void) | undefined;
|
||||
prefillPromoCode?: string;
|
||||
scrollIntoView?: boolean;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLFormElement | null>(null);
|
||||
const form = useForm<z.infer<typeof couponFormSchema>>({
|
||||
resolver: zodResolver(couponFormSchema),
|
||||
defaultValues: {
|
||||
promoCode: "",
|
||||
promoCode: props.prefillPromoCode,
|
||||
},
|
||||
});
|
||||
|
||||
const scrolled = useRef(false);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (props.scrollIntoView && !scrolled.current) {
|
||||
const el = containerRef.current;
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
el.querySelector("input")?.focus();
|
||||
scrolled.current = true;
|
||||
}
|
||||
}
|
||||
}, [props.scrollIntoView]);
|
||||
|
||||
const applyCoupon = useMutation({
|
||||
mutationFn: (promoCode: string) => props.submit(promoCode),
|
||||
});
|
||||
@@ -133,7 +154,7 @@ export function ApplyCouponCardUI(props: {
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} ref={containerRef}>
|
||||
<SettingsCard
|
||||
header={{
|
||||
title: "Apply Coupon",
|
||||
@@ -272,11 +293,7 @@ export function CouponSection(props: { teamId: string | undefined }) {
|
||||
});
|
||||
|
||||
if (activeCoupon.isPending) {
|
||||
return (
|
||||
<div className="flex h-[300px] items-center justify-center rounded-lg border border-border bg-muted/50">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
);
|
||||
return <LoadingCouponSection />;
|
||||
}
|
||||
|
||||
const couponData = optimisticCouponData
|
||||
@@ -296,17 +313,27 @@ export function CouponSection(props: { teamId: string | undefined }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ApplyCouponCard
|
||||
teamId={props.teamId}
|
||||
onCouponApplied={(coupon) => {
|
||||
setOptimisticCouponData({
|
||||
type: "added",
|
||||
data: coupon,
|
||||
});
|
||||
activeCoupon.refetch().then(() => {
|
||||
setOptimisticCouponData(undefined);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<LoadingCouponSection />}>
|
||||
<ApplyCouponCard
|
||||
teamId={props.teamId}
|
||||
onCouponApplied={(coupon) => {
|
||||
setOptimisticCouponData({
|
||||
type: "added",
|
||||
data: coupon,
|
||||
});
|
||||
activeCoupon.refetch().then(() => {
|
||||
setOptimisticCouponData(undefined);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingCouponSection() {
|
||||
return (
|
||||
<div className="flex h-[300px] items-center justify-center rounded-lg border border-border bg-muted/50">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export const BillingDowngradeDialog: React.FC<BillingDowngradeDialogProps> = ({
|
||||
<Flex flexDir="column" gap={2}>
|
||||
{oldPlanFeatures.map((feat) => (
|
||||
<div
|
||||
className="flex flex-row"
|
||||
className="flex flex-row items-center gap-2"
|
||||
key={Array.isArray(feat) ? feat[0] : feat}
|
||||
>
|
||||
<Icon as={FiX} boxSize={4} color="red.500" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flex, Icon } from "@chakra-ui/react";
|
||||
import { FiAlertCircle, FiCheckCircle, FiInfo } from "react-icons/fi";
|
||||
import { Badge, Heading, Text } from "tw-components";
|
||||
import { Badge, Text } from "tw-components";
|
||||
|
||||
interface BillingHeaderProps {
|
||||
validPayment: boolean;
|
||||
@@ -12,12 +12,10 @@ export const BillingHeader: React.FC<BillingHeaderProps> = ({
|
||||
paymentVerification,
|
||||
}) => {
|
||||
return (
|
||||
<Flex direction="column" gap={2}>
|
||||
<Heading size="title.lg" as="h1">
|
||||
Billing Info
|
||||
</Heading>
|
||||
<Flex direction="column" gap={1}>
|
||||
<h1 className="font-semibold text-xl tracking-tight">Billing Info</h1>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Text size="body.md">
|
||||
Manage your payment methods, billing information and invoices.
|
||||
</Text>
|
||||
@@ -29,7 +27,7 @@ export const BillingHeader: React.FC<BillingHeaderProps> = ({
|
||||
py={1.5}
|
||||
textTransform="capitalize"
|
||||
>
|
||||
<span className="flex flex-row">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<Icon
|
||||
as={
|
||||
validPayment
|
||||
|
||||
@@ -20,7 +20,7 @@ export const BillingPlan: React.FC<BillingPlanProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<Flex direction={direction} gap={3}>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Text color={titleColor} size={titleSize}>
|
||||
Your current plan is
|
||||
</Text>
|
||||
@@ -35,7 +35,7 @@ export const BillingPlan: React.FC<BillingPlanProps> = ({
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{description && <Text>{description}</Text>}
|
||||
|
||||
<TrackedLink
|
||||
|
||||
@@ -17,7 +17,7 @@ export const BillingPlanCard = () => {
|
||||
|
||||
return (
|
||||
<Card as={Flex} flexDir="column" gap={2}>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Heading size="title.xs">Your current plan is</Heading>
|
||||
<Badge
|
||||
borderRadius="md"
|
||||
|
||||
@@ -142,7 +142,7 @@ export const Billing: React.FC<BillingProps> = ({ account, teamId }) => {
|
||||
return [
|
||||
{
|
||||
title: (
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-2">
|
||||
<Heading
|
||||
size="label.md"
|
||||
opacity={!stepsCompleted.account ? 1 : 0.6}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { EyeIcon, FilePlusIcon } from "lucide-react";
|
||||
import { EyeIcon, FilePlusIcon, ImageIcon, UploadIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type FileRejection,
|
||||
useDropzone,
|
||||
} from "react-dropzone";
|
||||
import { FiImage, FiUpload } from "react-icons/fi";
|
||||
|
||||
interface IFileInputProps {
|
||||
accept?: Accept;
|
||||
@@ -127,7 +126,7 @@ export const FileInput: React.FC<IFileInputProps> = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
||||
<FiUpload className="size-6" />
|
||||
<UploadIcon className="size-6" />
|
||||
<p className="text-center text-sm">{isDisabledText}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +145,7 @@ export const FileInput: React.FC<IFileInputProps> = ({
|
||||
>
|
||||
{noDisplay ? (
|
||||
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
||||
<FiImage className="size-6" />
|
||||
<ImageIcon className="size-6" />
|
||||
<p className="text-sm">{fileType} uploaded</p>
|
||||
</div>
|
||||
) : fileUrl ? (
|
||||
@@ -162,7 +161,7 @@ export const FileInput: React.FC<IFileInputProps> = ({
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2.5 text-muted-foreground">
|
||||
<div className="flex items-center justify-center rounded-full border border-border bg-background p-3">
|
||||
<FiUpload className="size-5" />
|
||||
<UploadIcon className="size-5" />
|
||||
</div>
|
||||
{!disableHelperText && (
|
||||
<p className="text-center text-sm">
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
GridItem,
|
||||
Icon,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
@@ -22,9 +21,9 @@ import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { MoveRightIcon } from "lucide-react";
|
||||
import pluralize from "pluralize";
|
||||
import { type SetStateAction, useMemo } from "react";
|
||||
import { FiArrowRight } from "react-icons/fi";
|
||||
import { Button, TableContainer, Text } from "tw-components";
|
||||
|
||||
type TWQueryTableProps<TRowData, TInputData> = {
|
||||
@@ -122,7 +121,7 @@ export function TWQueryTable<TRowData, TInputData>(
|
||||
borderBottomWidth="inherit"
|
||||
borderBottomColor="hsl(var(--border))"
|
||||
>
|
||||
<Icon as={FiArrowRight} />
|
||||
<MoveRightIcon className="size-4" />
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
|
||||
@@ -27,10 +27,9 @@ import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { EllipsisVerticalIcon, MoveRightIcon } from "lucide-react";
|
||||
import pluralize from "pluralize";
|
||||
import { type SetStateAction, useMemo, useState } from "react";
|
||||
import { FaEllipsisVertical } from "react-icons/fa6";
|
||||
import { FiArrowRight } from "react-icons/fi";
|
||||
import type { IconType } from "react-icons/lib";
|
||||
|
||||
type CtaMenuItem<TRowData> = {
|
||||
@@ -191,7 +190,7 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
|
||||
{/* Show a ... menu or individual CTA buttons. */}
|
||||
{tableProps.onRowClick ? (
|
||||
<TableCell className="text-end">
|
||||
<FiArrowRight className="size-4" />
|
||||
<MoveRightIcon className="size-4" />
|
||||
</TableCell>
|
||||
) : tableProps.onMenuClick ? (
|
||||
<TableCell className="text-end">
|
||||
@@ -202,7 +201,7 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
|
||||
aria-label="Actions"
|
||||
className="!h-auto relative z-10 p-2.5"
|
||||
>
|
||||
<FaEllipsisVertical className="size-4" />
|
||||
<EllipsisVerticalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
|
||||
@@ -272,14 +272,14 @@ export function AccountAbstractionSettingsPage(
|
||||
>
|
||||
<FormControl>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-6 lg:gap-12">
|
||||
<div>
|
||||
<FormLabel pointerEvents="none">Global spend limits</FormLabel>
|
||||
<Text>
|
||||
Maximum gas cost (in USD) that you want to sponsor. This
|
||||
applies for the duration of the billing period (monthly). Once
|
||||
this limit is reached, your users will have to fund their own
|
||||
gas costs.
|
||||
Maximum gas cost (in USD) that you want to sponsor. <br />{" "}
|
||||
This applies for the duration of the billing period (monthly).
|
||||
Once this limit is reached, your users will have to fund their
|
||||
own gas costs.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -308,7 +308,7 @@ export function AccountAbstractionSettingsPage(
|
||||
}
|
||||
>
|
||||
<FormLabel>Spend limit</FormLabel>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Input
|
||||
w="xs"
|
||||
placeholder="Enter an amount"
|
||||
@@ -343,14 +343,14 @@ export function AccountAbstractionSettingsPage(
|
||||
}
|
||||
>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-6 lg:gap-12">
|
||||
<div>
|
||||
<FormLabel pointerEvents="none">
|
||||
Restrict to specific chains
|
||||
</FormLabel>
|
||||
<Text>
|
||||
Only sponsor transactions on the specified chains. By default,
|
||||
transactions can be sponsored on any of the{" "}
|
||||
Only sponsor transactions on the specified chains. <br /> By
|
||||
default, transactions can be sponsored on any of the{" "}
|
||||
<TrackedLink
|
||||
color="primary.500"
|
||||
isExternal
|
||||
@@ -399,7 +399,7 @@ export function AccountAbstractionSettingsPage(
|
||||
}
|
||||
>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-6 lg:gap-12">
|
||||
<div>
|
||||
<FormLabel pointerEvents="none">
|
||||
Restrict to specific contract addresses
|
||||
@@ -442,7 +442,7 @@ export function AccountAbstractionSettingsPage(
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-6 lg:gap-12">
|
||||
<div>
|
||||
<FormLabel pointerEvents="none">
|
||||
Allowlisted/Blocklisted accounts
|
||||
@@ -519,13 +519,14 @@ export function AccountAbstractionSettingsPage(
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-6 lg:gap-12">
|
||||
<div>
|
||||
<FormLabel pointerEvents="none">Server verifier</FormLabel>
|
||||
<Text>
|
||||
Specify your own endpoint that will verify each transaction
|
||||
and decide whether it should be sponsored or not. This gives
|
||||
you fine grained control and lets you build your own rules.{" "}
|
||||
and decide whether it should be sponsored or not. <br /> This
|
||||
gives you fine grained control and lets you build your own
|
||||
rules.{" "}
|
||||
<TrackedLink
|
||||
category={trackingCategory}
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet/sponsorship-rules#setting-up-a-server-verifier"
|
||||
@@ -634,7 +635,7 @@ export function AccountAbstractionSettingsPage(
|
||||
}
|
||||
>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-between gap-6 lg:gap-12">
|
||||
<div>
|
||||
<FormLabel pointerEvents="none">Admin accounts</FormLabel>
|
||||
<Text>
|
||||
|
||||
@@ -427,7 +427,7 @@ const SnapshotTableColumns = [
|
||||
return address;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Tooltip
|
||||
label={
|
||||
address.startsWith("0x")
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Divider,
|
||||
Flex,
|
||||
FormControl,
|
||||
Icon,
|
||||
LightMode,
|
||||
List,
|
||||
Select,
|
||||
@@ -26,10 +25,9 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CircleHelpIcon } from "lucide-react";
|
||||
import { ChevronDownIcon, CircleHelpIcon, CopyIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { FiChevronDown, FiCopy } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { stringify } from "thirdweb/utils";
|
||||
@@ -258,7 +256,7 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
toast.info("Transaction hash copied.");
|
||||
}}
|
||||
>
|
||||
<Icon as={FiCopy} boxSize={3} />
|
||||
<CopyIcon className="size-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Text fontFamily="mono" noOfLines={1}>
|
||||
@@ -306,7 +304,7 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
{transaction.blockNumber}
|
||||
</Text>
|
||||
<div>
|
||||
<Icon as={FiChevronDown} />
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
@@ -335,7 +335,7 @@ const AirdropTable: React.FC<AirdropTableProps> = ({ data, portalRef }) => {
|
||||
return address;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Tooltip
|
||||
label={
|
||||
address.startsWith("0x")
|
||||
|
||||
@@ -23,7 +23,7 @@ export const SupplyCards: React.FC<SupplyCardsProps> = ({ contract }) => {
|
||||
).toString();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-3 md:gap-6">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:gap-6">
|
||||
<Card as={Stat}>
|
||||
<StatLabel mb={{ base: 1, md: 0 }}>Total Supply</StatLabel>
|
||||
<Skeleton isLoaded={totalSupplyQuery.isSuccess}>
|
||||
|
||||
@@ -53,9 +53,9 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<h2 className="font-bold text-xl">Contract NFTs</h2>
|
||||
<div className="flex flex-col gap-2 md:flex-row">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<h2 className="font-semibold text-2xl tracking-tight">Contract NFTs</h2>
|
||||
<div className="grid grid-cols-2 gap-2 md:flex">
|
||||
{isRevealable && <NFTRevealButton contract={contract} />}
|
||||
{isERC721ClaimToSupported && (
|
||||
/**
|
||||
|
||||
@@ -50,7 +50,7 @@ export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link href={`${chainSlug}/${contractAddress}/analytics`}>
|
||||
<Link href={`/${chainSlug}/${contractAddress}/analytics`}>
|
||||
<span>View All</span>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
|
||||
@@ -266,7 +266,7 @@ export const BatchLazyMint: ComponentWithChildren<BatchLazyMintProps> = (
|
||||
w="100%"
|
||||
mb={2}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
className="text-muted-foreground"
|
||||
variant="ghost"
|
||||
|
||||
@@ -214,7 +214,7 @@ export const BatchTable: React.FC<BatchTableProps> = ({
|
||||
</TableContainer>
|
||||
<Portal containerRef={portalRef}>
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<IconButton
|
||||
isDisabled={!canPreviousPage}
|
||||
aria-label="first page"
|
||||
|
||||
@@ -32,11 +32,13 @@ export async function middleware(request: NextRequest) {
|
||||
// check if the user is logged in (has a valid auth cookie)
|
||||
|
||||
if (!authCookie) {
|
||||
const searchParamsString = request.nextUrl.searchParams.toString();
|
||||
|
||||
// if not logged in, rewrite to login page
|
||||
return redirect(
|
||||
request,
|
||||
"/login",
|
||||
`next=${encodeURIComponent(pathname)}`,
|
||||
`next=${encodeURIComponent(`${pathname}${searchParamsString ? `?${searchParamsString}` : ""}`)}`,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { ThirdwebNextPage } from "utils/types";
|
||||
import chakraTheme from "../theme";
|
||||
import "@/styles/globals.css";
|
||||
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
|
||||
import { TryTeamsUIBanner } from "../app/components/DashboardTypeBanner";
|
||||
import { UnlimitedWalletsBanner } from "../components/notices/AnnouncementBanner";
|
||||
|
||||
const inter = interConstructor({
|
||||
subsets: ["latin"],
|
||||
@@ -260,8 +260,7 @@ const ConsoleApp = memo(function ConsoleApp({
|
||||
/>
|
||||
|
||||
<DashboardRouterTopProgressBar />
|
||||
|
||||
<TryTeamsUIBanner />
|
||||
<UnlimitedWalletsBanner />
|
||||
|
||||
<TailwindTheme>
|
||||
<ChakraProvider theme={chakraThemeWithFonts}>
|
||||
|
||||
@@ -55,19 +55,34 @@ export function Profiles() {
|
||||
|
||||
function App() {
|
||||
const { mutate: linkProfile, isPending, error } = useLinkProfile();
|
||||
|
||||
const linkMetamask = () => {
|
||||
// link any external wallet
|
||||
linkProfile({
|
||||
client: THIRDWEB_CLIENT,
|
||||
strategy: "wallet",
|
||||
wallet: createWallet("io.metamask"), // or any other wallet
|
||||
chain: baseSepolia,
|
||||
});
|
||||
};
|
||||
|
||||
const linkPasskey = () => {
|
||||
// link any web2 identity provider
|
||||
linkProfile({
|
||||
client: THIRDWEB_CLIENT,
|
||||
strategy: "passkey", // or "email", "phone", etc.
|
||||
type: "sign-up",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => linkProfile({
|
||||
client: THIRDWEB_CLIENT,
|
||||
strategy: "wallet",
|
||||
wallet: createWallet("com.coinbase.wallet"),
|
||||
chain: baseSepolia,
|
||||
})}
|
||||
>
|
||||
Link Coinbase Wallet
|
||||
</button>
|
||||
<button onClick={linkMetamask}>
|
||||
Link Metamask
|
||||
</button>
|
||||
<button onClick={linkPasskey}>
|
||||
Link Passkey
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};`}
|
||||
|
||||
@@ -109,7 +109,7 @@ Here's an example of a 2-hour timeout:
|
||||
|
||||
### How do I customize my RPC?
|
||||
|
||||
Use [Update Chain Configuration](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Configuration/operation/updateChainsConfiguration) to override a chain's settings.
|
||||
Use [Update Chain Configuration](https://thirdweb-engine.apidocumentation.com/reference#tag/configuration/POST/configuration/chains) to override a chain's settings.
|
||||
|
||||
**Example**
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ When Engine receives a cancellation request:
|
||||
|
||||
**When canceled, the transaction will not be re-attempted.** Your backend may safely re-attempt this transaction.
|
||||
|
||||
_Note: Use [Retry Transaction](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Transaction/operation/retry) instead to increase the gas settings for a queued transaction._
|
||||
|
||||
## Cancel a transaction from the API
|
||||
|
||||
```typescript
|
||||
@@ -44,7 +42,7 @@ if (resp.status === 200) {
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [`Cancel Transaction`](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Transaction/operation/cancel)
|
||||
Reference: [`Cancel Transaction`](https://thirdweb-engine.apidocumentation.com/reference#tag/transaction/POST/transaction/cancel)
|
||||
|
||||
## Cancel a transaction from the dashboard
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Your app may trigger an action when an onchain event occurs, such as:
|
||||
1. Select **Add Contract Subscription**.
|
||||
1. Provide a webhook URL.
|
||||
|
||||
API reference: [`POST /contract-subscriptions/add`](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Contract-Subscriptions/operation/addContractSubscription)
|
||||
API reference: [`POST /contract-subscriptions/add`](https://thirdweb-engine.apidocumentation.com/reference#tag/contract-subscriptions/POST/contract-subscriptions/add)
|
||||
|
||||
#### Remove a Contract Subscription
|
||||
|
||||
@@ -46,7 +46,7 @@ API reference: [`POST /contract-subscriptions/add`](https://redocly.github.io/re
|
||||
1. Select **Contract Subscriptions**.
|
||||
1. Select **... > Remove** next to an existing subscription.
|
||||
|
||||
API reference: [`POST /contract-subscriptions/remove`](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Contract-Subscriptions/operation/removeContractSubscription)
|
||||
API reference: [`POST /contract-subscriptions/remove`](https://thirdweb-engine.apidocumentation.com/reference#tag/contract-subscriptions/POST/contract-subscriptions/remove)
|
||||
|
||||
## Handling webhooks
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ This is the request body to call the second function:
|
||||
|
||||
</Details>
|
||||
|
||||
[See API reference](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Contract/operation/write)
|
||||
[See API reference](https://thirdweb-engine.apidocumentation.com/reference#tag/contract/POST/contract/{chain}/{contractAddress}/write)
|
||||
|
||||
### Call a payable method
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export const sidebar: SideBar = {
|
||||
},
|
||||
{
|
||||
name: "API Reference",
|
||||
href: "https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json",
|
||||
href: "https://thirdweb-engine.apidocumentation.com/",
|
||||
},
|
||||
{
|
||||
name: "Typescript SDK",
|
||||
|
||||
@@ -60,9 +60,9 @@ Engine automatically manages nonces so this error typically should not occur.
|
||||
|
||||
It's possible the transactions are not being accepted by the mempool. To remediate this issue, you can retry a transaction, cancel it, or reset all backend wallet nonces.
|
||||
|
||||
To retry a transaction, use [`POST /transaction/sync-retry`](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Transaction/operation/syncRetry) and provide the `queueId`. You may also provide a higher gas value to increase the likelihood of the transaction being mined.
|
||||
To retry a transaction, use [`POST /transaction/sync-retry`](https://thirdweb-engine.apidocumentation.com/reference#tag/transaction/POST/transaction/sync-retry) and provide the `queueId`. You may also provide a higher gas value to increase the likelihood of the transaction being mined.
|
||||
|
||||
To cancel a transaction, use [`POST /transaction/cancel`](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Transaction/operation/cancel) and provide the `queueId`.
|
||||
To cancel a transaction, use [`POST /transaction/cancel`](https://thirdweb-engine.apidocumentation.com/reference#tag/transaction/POST/transaction/cancel) and provide the `queueId`.
|
||||
|
||||
|
||||
<Callout
|
||||
@@ -72,7 +72,7 @@ To cancel a transaction, use [`POST /transaction/cancel`](https://redocly.github
|
||||
Transactions that were sent but not yet mined may fail because new transactions will use the same nonce values as previously sent transactions.
|
||||
</Callout>
|
||||
|
||||
To reset the nonce, use [`POST /backend-wallet/reset-nonce`](https://redocly.github.io/redoc/?url=https://demo.web3api.thirdweb.com/json#tag/Backend-Wallet/operation/resetNonces) to sync the Engine nonce with the onchain value. This step resets all nonce values tracked in Engine, and the next sent transaction will use the next available onchain nonce.
|
||||
To reset the nonce for all backend wallets, use [`POST /backend-wallet/reset-nonce`](https://thirdweb-engine.apidocumentation.com/reference#tag/backend-wallet/POST/backend-wallet/reset-nonces) to force-sync the Engine nonces with the onchain values.
|
||||
|
||||
### Received "Insufficient funds for gas * price + value" error
|
||||
|
||||
@@ -93,4 +93,31 @@ Even though the transaction is expected to consume less gas, the transaction wil
|
||||
|
||||
**How to resolve**
|
||||
|
||||
Add more funds to your backend wallet. Or wait until the gas fees on the chain are lower and try again.
|
||||
Add more funds to your backend wallet. Or wait until the gas fees on the chain are lower and try again.
|
||||
|
||||
## Sending UserOps
|
||||
|
||||
UserOps are **User Op**erations sent by a backend wallet on behalf of smart accounts, used in the Account Abstraction pattern.
|
||||
|
||||
### Received "Invalid UserOperation signature or paymaster signature" error
|
||||
|
||||
*Also known as: code -32507*
|
||||
|
||||
**What it means**
|
||||
|
||||
The backend wallet you are sending the transaction with does not have sufficient permissions to send a userOp for this account address.
|
||||
|
||||
**How to resolve**
|
||||
|
||||
Confirm if the backend wallet is a valid admin for this account.
|
||||
If you are using Session Keys, confirm they are not close to or already expired.
|
||||
|
||||
### Received "AA21 didn’t pay prefund" error
|
||||
|
||||
**What it means**
|
||||
|
||||
Your thirdweb API key used by Engine is not properly configured to use Account Abstraction, or you have a [sponsorship rule](https://portal.thirdweb.com/connect/account-abstraction/sponsorship-rules) that has been exceeded.
|
||||
|
||||
**How to resolve**
|
||||
|
||||
Confirm if the API key is tied to an account with valid payment. Also confirm if there are any sponsorship rule for the Engine API key.
|
||||
@@ -159,12 +159,24 @@ If the Expo CLI says "Using Expo Go" when starting the app, press `s` to switch
|
||||
|
||||
</Callout>
|
||||
|
||||
Examples:
|
||||
Prebuild the app (only needed once):
|
||||
|
||||
```shell
|
||||
npx expo prebuild
|
||||
```
|
||||
|
||||
Run the development build on ios:
|
||||
|
||||
```shell
|
||||
npx expo:run ios
|
||||
```
|
||||
|
||||
Run the development build on android:
|
||||
|
||||
```shell
|
||||
npx expo:run android
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title=" Use the `thirdweb` package normally">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Book, CodeIcon, ZapIcon } from "lucide-react";
|
||||
import { Book, BugIcon, CodeIcon, ZapIcon } from "lucide-react";
|
||||
import type { SideBar } from "../../../components/Layouts/DocLayout";
|
||||
import { ReactIcon, TypeScriptIcon } from "../../../icons";
|
||||
import { fetchTypeScriptDoc } from "../../references/components/TDoc/fetchDocs/fetchTypeScriptDoc";
|
||||
@@ -22,6 +22,11 @@ export const sidebar: SideBar = {
|
||||
href: `${slug}/getting-started`,
|
||||
icon: <ZapIcon />,
|
||||
},
|
||||
{
|
||||
name: "Troubleshooting",
|
||||
href: `${slug}/troubleshooting`,
|
||||
icon: <BugIcon />,
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
name: "Core",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Expo Go
|
||||
|
||||
Due to required native dependencies, you cannot use Expo Go for running your app. You must use a development build.
|
||||
|
||||
You can create ios/android development builds using the following command:
|
||||
|
||||
```shell
|
||||
npx expo prebuild
|
||||
```
|
||||
|
||||
You can then run the development build using the following command:
|
||||
|
||||
```shell
|
||||
npx expo:run ios
|
||||
```
|
||||
|
||||
```shell
|
||||
npx expo:run android
|
||||
```
|
||||
|
||||
## OpenSSL iOS build error
|
||||
|
||||
If you see a build error that mentions OpenSSL, you may need to update `app.json` to pin a version of OpenSSL that is compatible with your Xcode version.
|
||||
|
||||
Here's how to pin the OpenSSL version in `app.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"plugins": [
|
||||
"expo-build-properties",
|
||||
{
|
||||
"ios": {
|
||||
"extraPods": [
|
||||
{
|
||||
"name": "OpenSSL-Universal",
|
||||
"configurations": ["Release", "Debug"],
|
||||
"modular_headers": true,
|
||||
"version": "<OPENSSL_VERSION>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `<OPENSSL_VERSION>` with the correct version of OpenSSL for your Xcode version:
|
||||
|
||||
- Xcode 16: `3.3.2000`
|
||||
- Xcode 15: `3.1.5004`
|
||||
|
||||
Make sure you have `expo-build-properties` added to your `package.json` dependencies.
|
||||
|
||||
## Android minSdkVersion error
|
||||
|
||||
If you see an Android error mentioning `minSdkVersion`, you may need to update your `app.json` to set the `minSdkVersion` to a higher version.
|
||||
|
||||
Here's how to update the `minSdkVersion` in `app.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"plugins": [
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"minSdkVersion": 26
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Make sure you have `expo-build-properties` added to your `package.json` dependencies.
|
||||
+1
-9
@@ -1,11 +1,9 @@
|
||||
import { ChainCombobox } from "@/components/ChainCombobox";
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
import { getChains } from "@/lib/chains";
|
||||
import { client } from "@/lib/client";
|
||||
import { getEcosystemInfo } from "@/lib/ecosystems";
|
||||
import { SIMPLEHASH_NFT_SUPPORTED_CHAIN_IDS } from "@/util/simplehash";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { resolveName } from "thirdweb/extensions/ens";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
|
||||
@@ -34,23 +32,17 @@ export default async function Layout({
|
||||
children: React.ReactNode;
|
||||
params: { ecosystem: string; address: string };
|
||||
}) {
|
||||
const userAddressPromise = getCurrentUser();
|
||||
const ensPromise = resolveName({
|
||||
client,
|
||||
address: params.address,
|
||||
});
|
||||
const thirdwebChainsPromise = getChains();
|
||||
|
||||
const [userAddress, ens, thirdwebChains] = await Promise.all([
|
||||
userAddressPromise,
|
||||
const [ens, thirdwebChains] = await Promise.all([
|
||||
ensPromise,
|
||||
thirdwebChainsPromise,
|
||||
]);
|
||||
|
||||
if (userAddress !== params.address) {
|
||||
redirect(`/wallet/${userAddress}`);
|
||||
}
|
||||
|
||||
const simpleHashChains = thirdwebChains.filter((chain) =>
|
||||
SIMPLEHASH_NFT_SUPPORTED_CHAIN_IDS.includes(chain.chainId),
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AutoConnectWalletConnect } from "@/components/AutoConnectWalletConnect";
|
||||
import NftGallery from "@/components/NftGallery";
|
||||
import { getAddress } from "thirdweb";
|
||||
|
||||
export default function Page({
|
||||
params: { address },
|
||||
searchParams: { chainId, uri },
|
||||
}: {
|
||||
params: { address: string };
|
||||
searchParams: { chainId?: string; uri?: string };
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<AutoConnectWalletConnect uri={uri} />
|
||||
<NftGallery owner={getAddress(address)} chainId={Number(chainId)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import NftGallery from "@/components/NftGallery";
|
||||
import { getAddress } from "thirdweb";
|
||||
|
||||
export default async function Page({
|
||||
params: { address },
|
||||
searchParams: { chainId },
|
||||
}: {
|
||||
params: { address: string };
|
||||
searchParams: { chainId: string };
|
||||
}) {
|
||||
return <NftGallery owner={getAddress(address)} chainId={Number(chainId)} />;
|
||||
}
|
||||
@@ -5,9 +5,7 @@ export default async function Layout({
|
||||
children,
|
||||
}: { children: React.ReactNode }) {
|
||||
const userAddress = await getCurrentUser();
|
||||
console.log("userAddress", userAddress);
|
||||
if (userAddress) {
|
||||
console.log("redirecting to wallet", userAddress);
|
||||
redirect(`/wallet/${userAddress}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,5 @@
|
||||
"use client";
|
||||
import { generatePayload, getCurrentUser, login, logout } from "@/lib/auth";
|
||||
import { client } from "@/lib/client";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { VerifyLoginPayloadParams } from "thirdweb/auth";
|
||||
import { ConnectEmbed } from "thirdweb/react";
|
||||
import { ecosystemWallet } from "thirdweb/wallets";
|
||||
import { ConnectEmbed } from "@/components/ConnectEmbed";
|
||||
|
||||
export default function Page({ params }: { params: { ecosystem: string } }) {
|
||||
const { theme } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<ConnectEmbed
|
||||
theme={theme === "light" ? "light" : "dark"}
|
||||
autoConnect={true}
|
||||
wallets={[ecosystemWallet(`ecosystem.${params.ecosystem}`)]}
|
||||
client={client}
|
||||
auth={{
|
||||
getLoginPayload: generatePayload,
|
||||
doLogin: async (loginParams: VerifyLoginPayloadParams) => {
|
||||
const success = await login(loginParams);
|
||||
if (success) {
|
||||
router.push(`/wallet/${loginParams.payload.address}`);
|
||||
}
|
||||
},
|
||||
isLoggedIn: async () => !!(await getCurrentUser()),
|
||||
doLogout: logout,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
export default function Page() {
|
||||
return <ConnectEmbed />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// This page is to accept a Wallet Connect request
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page({
|
||||
searchParams: { uri },
|
||||
}: {
|
||||
searchParams: { uri: string };
|
||||
}) {
|
||||
const currentUser = await getCurrentUser();
|
||||
|
||||
if (!currentUser) {
|
||||
redirect(`/login?uri=${encodeURIComponent(uri)}`);
|
||||
}
|
||||
|
||||
redirect(`/wallet/${currentUser}?uri=${encodeURIComponent(uri)}`);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useWalletConnect } from "@/hooks/useWalletConnect";
|
||||
|
||||
export function AutoConnectWalletConnect({ uri }: { uri?: string }) {
|
||||
useWalletConnect({ uri });
|
||||
return <></>;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
import { generatePayload, isLoggedIn, login, logout } from "@/lib/auth";
|
||||
import { client } from "@/lib/client";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
@@ -24,14 +23,6 @@ export default function ConnectButton({
|
||||
wallets={[ecosystemWallet(ecosystem)]}
|
||||
client={client}
|
||||
theme={theme === "light" ? "light" : "dark"}
|
||||
auth={{
|
||||
getLoginPayload: generatePayload,
|
||||
doLogin: async (p) => {
|
||||
login(p);
|
||||
},
|
||||
isLoggedIn,
|
||||
doLogout: logout,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
import { generatePayload, getCurrentUser, login, logout } from "@/lib/auth";
|
||||
import { client } from "@/lib/client";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import type { VerifyLoginPayloadParams } from "thirdweb/auth";
|
||||
import { ConnectEmbed as ThirdwebConnectEmbed } from "thirdweb/react";
|
||||
import { ecosystemWallet } from "thirdweb/wallets";
|
||||
|
||||
export function ConnectEmbed() {
|
||||
const { theme } = useTheme();
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return (
|
||||
<ThirdwebConnectEmbed
|
||||
theme={theme === "light" ? "light" : "dark"}
|
||||
autoConnect={true}
|
||||
wallets={[ecosystemWallet(`ecosystem.${params.ecosystem}`)]}
|
||||
client={client}
|
||||
auth={{
|
||||
getLoginPayload: generatePayload,
|
||||
doLogin: async (loginParams: VerifyLoginPayloadParams) => {
|
||||
const success = await login(loginParams);
|
||||
if (success) {
|
||||
router.push(
|
||||
`/wallet/${loginParams.payload.address}?${searchParams.toString()}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
isLoggedIn: async () => !!(await getCurrentUser()),
|
||||
doLogout: logout,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { client } from "@/lib/client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useActiveWallet } from "thirdweb/react";
|
||||
import {
|
||||
createWalletConnectClient,
|
||||
createWalletConnectSession,
|
||||
} from "thirdweb/wallets";
|
||||
|
||||
export function useWalletConnect({ uri }: { uri?: string }) {
|
||||
const wallet = useActiveWallet();
|
||||
|
||||
useQuery({
|
||||
queryKey: ["wallet-connect", uri],
|
||||
queryFn: async () => {
|
||||
if (!wallet || !uri) throw new Error("Unreachable");
|
||||
const wcClient = await createWalletConnectClient({
|
||||
wallet: wallet,
|
||||
client: client,
|
||||
});
|
||||
|
||||
createWalletConnectSession({
|
||||
walletConnectClient: wcClient,
|
||||
uri,
|
||||
});
|
||||
|
||||
toast.success("Wallet connected.", {
|
||||
id: "wallet-connect",
|
||||
icon: <CheckIcon className="h-4 w-4" />,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
enabled: !!uri || !!wallet,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1,5 +1,27 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.61.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4962](https://github.com/thirdweb-dev/js/pull/4962) [`9685a85`](https://github.com/thirdweb-dev/js/commit/9685a85274ac3ac1f999e8f552eff78fe2027b45) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Expose UnstoppableDomains extensions: namehash and reverseNameOf
|
||||
|
||||
- [#5004](https://github.com/thirdweb-dev/js/pull/5004) [`df0388d`](https://github.com/thirdweb-dev/js/commit/df0388d83b9c413b593f64ed5fe6c3c707a634d6) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose auth options on `useConnectModal`
|
||||
|
||||
- [#4961](https://github.com/thirdweb-dev/js/pull/4961) [`98f358d`](https://github.com/thirdweb-dev/js/commit/98f358d63a5c33a72cfe52bcbd910ea39fb2f33e) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Throw an error if an expected auth token is missing
|
||||
|
||||
- [#5002](https://github.com/thirdweb-dev/js/pull/5002) [`cd2c0f3`](https://github.com/thirdweb-dev/js/commit/cd2c0f3868bc9d0509130cc7664e6825759e9e78) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Handle zk sync direct deploys in `deployContract`
|
||||
|
||||
## 5.61.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4979](https://github.com/thirdweb-dev/js/pull/4979) [`ccd4cd6`](https://github.com/thirdweb-dev/js/commit/ccd4cd6f59423d417757e86bf04a5a07b061e2f9) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Overestimate zksync gas limit to account for paymaster extra gas
|
||||
|
||||
- [#4974](https://github.com/thirdweb-dev/js/pull/4974) [`6a4b776`](https://github.com/thirdweb-dev/js/commit/6a4b776eccc16203795786d44c4b80c011c2af37) Thanks [@ElasticBottle](https://github.com/ElasticBottle)! - fix: allow account linking on thirdweb dashboard
|
||||
|
||||
- [#4977](https://github.com/thirdweb-dev/js/pull/4977) [`7c10343`](https://github.com/thirdweb-dev/js/commit/7c103431c4e4788277086db1f21e32833b6dfd2b) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Add soneiumMinato chain definition
|
||||
|
||||
## 5.61.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.61.4",
|
||||
"version": "5.61.6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
[
|
||||
"function encodeBytesOnInstall() pure returns (bytes)",
|
||||
"function getAllMetadataBatches() view returns ((uint256 startTokenIdInclusive, uint256 endTokenIdInclusive, string baseURI)[])",
|
||||
"function getBatchIndex(uint256 _tokenId) view returns (uint256)",
|
||||
"function getMetadataBatch(uint256 _batchIndex) view returns ((uint256 startTokenIdInclusive, uint256 endTokenIdInclusive, string baseURI))",
|
||||
"function getModuleConfig() pure returns ((bool registerInstallationCallback, bytes4[] requiredInterfaces, bytes4[] supportedInterfaces, (bytes4 selector)[] callbackFunctions, (bytes4 selector, uint256 permissionBits)[] fallbackFunctions) config)",
|
||||
"function setBaseURI(uint256 _batchIndex, string _baseURI)",
|
||||
"function uploadMetadata(uint256 _amount, string _baseURI)",
|
||||
"event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId)"
|
||||
"event BatchMetadataUpdate(uint256 startTokenIdIncluside, uint256 endTokenIdInclusive, string baseURI)"
|
||||
]
|
||||
@@ -1,6 +1,10 @@
|
||||
[
|
||||
"function encodeBytesOnInstall() pure returns (bytes)",
|
||||
"function getAllMetadataBatches() view returns ((uint256 startTokenIdInclusive, uint256 endTokenIdInclusive, string baseURI)[])",
|
||||
"function getBatchIndex(uint256 _tokenId) view returns (uint256)",
|
||||
"function getMetadataBatch(uint256 _batchIndex) view returns ((uint256 startTokenIdInclusive, uint256 endTokenIdInclusive, string baseURI))",
|
||||
"function getModuleConfig() pure returns ((bool registerInstallationCallback, bytes4[] requiredInterfaces, bytes4[] supportedInterfaces, (bytes4 selector)[] callbackFunctions, (bytes4 selector, uint256 permissionBits)[] fallbackFunctions) config)",
|
||||
"function setBaseURI(uint256 _batchIndex, string _baseURI)",
|
||||
"function uploadMetadata(uint256 _amount, string _baseURI)",
|
||||
"event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId)"
|
||||
"event BatchMetadataUpdate(uint256 startTokenIdIncluside, uint256 endTokenIdInclusive, string baseURI)"
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
"function encodeBytesOnInstall() pure returns (bytes)",
|
||||
"function encodeBytesOnInstall(uint256 startTokenId) pure returns (bytes)",
|
||||
"function getModuleConfig() pure returns ((bool registerInstallationCallback, bytes4[] requiredInterfaces, bytes4[] supportedInterfaces, (bytes4 selector)[] callbackFunctions, (bytes4 selector, uint256 permissionBits)[] fallbackFunctions) config)",
|
||||
"function getNextTokenId() view returns (uint256)",
|
||||
"error SequentialTokenIdInvalidTokenId()",
|
||||
"error UpdateTokenIdCallbackERC1155NotImplemented()"
|
||||
"function updateTokenIdERC1155(uint256 _tokenId) payable returns (uint256)"
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
[
|
||||
"function reverseNameOf(address addr) view returns (string reverseUri)",
|
||||
"function getMany(string[] keys, uint256 tokenId) view returns (string[] values)",
|
||||
"function namehash(string[] labels) pure returns (uint256 hash)"
|
||||
"function namehash(string[] labels) pure returns (uint256 hash)",
|
||||
"function exists(uint256 tokenId) view returns (bool)"
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineChain } from "../utils.js";
|
||||
|
||||
/**
|
||||
* @chain
|
||||
*/
|
||||
export const soneiumMinato = /* @__PURE__ */ defineChain({
|
||||
id: 1946,
|
||||
name: "Soneium Minato",
|
||||
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
||||
blockExplorers: [
|
||||
{
|
||||
name: "Minato Explorer",
|
||||
url: "https://explorer-testnet.soneium.org/",
|
||||
},
|
||||
],
|
||||
testnet: true,
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { encodeAbiParameters } from "../../utils/abi/encodeAbiParameters.js";
|
||||
import { normalizeFunctionParams } from "../../utils/abi/normalizeFunctionParams.js";
|
||||
import { computeDeploymentAddress } from "../../utils/any-evm/compute-deployment-address.js";
|
||||
import { computeDeploymentInfoFromBytecode } from "../../utils/any-evm/compute-published-contract-deploy-info.js";
|
||||
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
|
||||
import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js";
|
||||
import { ensureBytecodePrefix } from "../../utils/bytecode/prefix.js";
|
||||
import { concatHex } from "../../utils/encoding/helpers/concat-hex.js";
|
||||
@@ -13,6 +14,7 @@ import type { Prettify } from "../../utils/type-utils.js";
|
||||
import type { ClientAndChain } from "../../utils/types.js";
|
||||
import type { Account } from "../../wallets/interfaces/wallet.js";
|
||||
import { getContract } from "../contract.js";
|
||||
import { zkDeployContract } from "./zksync/zkDeployContract.js";
|
||||
|
||||
/**
|
||||
* @extension DEPLOY
|
||||
@@ -121,6 +123,18 @@ export async function deployContract(
|
||||
salt?: string;
|
||||
},
|
||||
) {
|
||||
if (await isZkSyncChain(options.chain)) {
|
||||
return zkDeployContract({
|
||||
account: options.account,
|
||||
client: options.client,
|
||||
chain: options.chain,
|
||||
bytecode: options.bytecode,
|
||||
abi: options.abi,
|
||||
params: options.constructorParams,
|
||||
salt: options.salt,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.salt !== undefined) {
|
||||
// Deploy with CREATE2 if salt is provided
|
||||
const info = await computeDeploymentInfoFromBytecode(options);
|
||||
|
||||
@@ -77,3 +77,4 @@ export { celoAlfajoresTestnet } from "../chains/chain-definitions/celo-alfajores
|
||||
export { fraxtalTestnet } from "../chains/chain-definitions/fraxtal-testnet.js";
|
||||
export { metalL2Testnet } from "../chains/chain-definitions/metal-l2-testnet.js";
|
||||
export { modeTestnet } from "../chains/chain-definitions/mode-testnet.js";
|
||||
export { soneiumMinato } from "../chains/chain-definitions/soneium-minato.js";
|
||||
|
||||
@@ -7,3 +7,12 @@ export {
|
||||
type ResolveAddressOptions,
|
||||
resolveAddress,
|
||||
} from "../../extensions/unstoppable-domains/read/resolveAddress.js";
|
||||
export {
|
||||
// Need this to resolve the tokenId, so that Social SDK can fetch the owner's Avatar from it
|
||||
namehash,
|
||||
type NamehashParams,
|
||||
} from "../../extensions/unstoppable-domains/__generated__/UnstoppableDomains/read/namehash.js";
|
||||
export {
|
||||
reverseNameOf,
|
||||
type ReverseNameOfParams,
|
||||
} from "../../extensions/unstoppable-domains/__generated__/UnstoppableDomains/read/reverseNameOf.js";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
|
||||
import { resolveAvatar } from "./resolve-avatar.js";
|
||||
|
||||
@@ -22,9 +21,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("ENS:resolve-avatar", () => {
|
||||
client: TEST_CLIENT,
|
||||
name: "vitalik.eth",
|
||||
});
|
||||
expect(avatarUri?.split("/ipfs/")[1]).toMatchInlineSnapshot(
|
||||
`"QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif"`,
|
||||
);
|
||||
expect(avatarUri).toMatchInlineSnapshot(`"https://euc.li/vitalik.eth"`);
|
||||
});
|
||||
|
||||
it("resolves name without avatar record to null", async () => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DOODLES_CONTRACT, USDT_CONTRACT } from "~test/test-contracts.js";
|
||||
import { getCurrencyMetadata } from "./getCurrencyMetadata.js";
|
||||
|
||||
describe("getCurrencyMetadata", () => {
|
||||
it("should throw if not a valid ERC20 contract", async () => {
|
||||
await expect(() =>
|
||||
getCurrencyMetadata({ contract: DOODLES_CONTRACT }),
|
||||
).rejects.toThrowError("Invalid currency token");
|
||||
});
|
||||
|
||||
it("should return valid result if the contract is ERC20", async () => {
|
||||
const result = await getCurrencyMetadata({ contract: USDT_CONTRACT });
|
||||
expect(result).toStrictEqual({
|
||||
name: "Tether USD",
|
||||
symbol: "USDT",
|
||||
decimals: 6,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { type Abi, toFunctionSelector } from "viem";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BASE_USDC_PROXY_CONTRACT,
|
||||
DOODLES_CONTRACT,
|
||||
USDT_CONTRACT,
|
||||
} from "~test/test-contracts.js";
|
||||
import { resolveContractAbi } from "../../../contract/actions/resolve-abi.js";
|
||||
import { isERC20 } from "./isERC20.js";
|
||||
|
||||
describe("isERC20", () => {
|
||||
it("should detect USDT as a valid erc20 contract", async () => {
|
||||
const abi = await resolveContractAbi<Abi>(USDT_CONTRACT);
|
||||
const selectors = abi
|
||||
.filter((f) => f.type === "function")
|
||||
.map((f) => toFunctionSelector(f));
|
||||
expect(isERC20(selectors)).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect USDC as a valid erc20 contract", async () => {
|
||||
const abi = await resolveContractAbi<Abi>(BASE_USDC_PROXY_CONTRACT);
|
||||
const selectors = abi
|
||||
.filter((f) => f.type === "function")
|
||||
.map((f) => toFunctionSelector(f));
|
||||
expect(isERC20(selectors)).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT detect any NFT contract as a valid erc20 contract", async () => {
|
||||
const abi = await resolveContractAbi<Abi>(DOODLES_CONTRACT);
|
||||
const selectors = abi
|
||||
.filter((f) => f.type === "function")
|
||||
.map((f) => toFunctionSelector(f));
|
||||
expect(isERC20(selectors)).toBe(false);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user