Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ca65b43566 Version Packages (#7467)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-28 23:57:39 -07:00
Corey RiceandGitHub 1f72396d7f docs: Update imports in react v5 adaptor examples (#7469) 2025-06-29 10:25:27 +12:00
MananTank 08834f6385 Dashboard: Update team layout (#7464)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring and updating the billing and settings-related components in the dashboard application, changing URLs from `/settings` to `/billing`, and enhancing the user interface for better clarity and usability.

### Detailed summary
- Deleted several unused files related to settings and credits.
- Updated links in various components to redirect from `/settings` to `/billing`.
- Refactored `useActiveSidebarLink` to streamline sidebar link management.
- Enhanced UI elements for better responsiveness and aesthetics.
- Introduced a new `StaffModeNotice` component for staff mode indications.
- Adjusted layout components for consistent styling.
- Reorganized billing-related components into a unified structure.
- Improved pagination button styles for better visibility.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/usage/rpc/page.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Introduced a full-width sidebar layout with collapsible submenus and responsive navigation.
  * Added a team sidebar layout with structured navigation for team dashboards.
  * Added collapsible UI components for use in layouts and menus.
  * Added a reusable StaffModeNotice component for staff mode indication.

* **Bug Fixes**
  * Improved container sizing and maximum width constraints for better layout consistency across analytics, audit log, and ecosystem pages.

* **Refactor**
  * Simplified and reorganized sidebar layouts, removing complex internal logic and mobile navigation from several components.
  * Updated navigation structures to use new sidebar and tab-based layouts.
  * Streamlined and restyled headers, banners, and button components for a more cohesive look.
  * Replaced inline staff mode banners with the StaffModeNotice component.
  * Removed sidebar navigation from usage and settings layouts for a cleaner interface.
  * Removed gas credits settings and related components from the settings area.
  * Refactored ecosystem pages to redirect directly to creation, removing landing page.
  * Updated billing-related URL paths by removing deprecated `/settings` segments for consistency.
  * Removed account plan constants and unused hooks for cleaner codebase.
  * Cleaned up imports and replaced some relative imports with absolute paths.

* **Style**
  * Enhanced visual styling and responsiveness for range selectors, sidebars, and headers.
  * Adjusted text sizes and spacing in mobile navigation and file sections for improved readability.
  * Added rounded corners and adjusted spacing in date and interval selectors.
  * Added responsive minimum widths and consistent borders to pagination buttons and links.

* **Chores**
  * Added a new dependency for collapsible UI support.
  * Updated import paths and added lint suppression comments for code clarity and maintainability.
  * Added responsive minimum widths to pagination buttons and consistent border styling to pagination links.
  * Updated multiple URLs in links and buttons to reflect new billing paths without `/settings` segment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-28 12:10:06 +00:00
Joaquim VergesandGitHub 143ab6d127 [Portal] simplify get started wallet documentation (#7466) 2025-06-28 04:10:53 -07:00
bc512551ab [SDK] Add enableCard prop to control fiat payment visibility (#7465)
Co-authored-by: Claude <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2025-06-28 04:09:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e29d48426 chore(deps): bump @vercel/functions from 2.1.0 to 2.2.2 (#7422)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-28 01:09:38 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e7e6446e7d chore(deps-dev): bump checkly from 5.6.0 to 6.0.1 (#7424)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-28 01:09:21 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7113f7c669 Version Packages (#7463)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-27 18:56:26 -07:00
f9d7935d84 Update engine API (#7427)
Signed-off-by: Prithvish Baidya <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
2025-06-27 18:41:37 -07:00
FirekeeperandGitHub acd5656d4d [SDK] EIP-7702 Session Keys (#7432) 2025-06-27 18:11:19 -07:00
gregandGitHub 6d1d344c48 Fix: Exclude IAW when smart wallets are used (#7454) 2025-06-27 13:36:11 -07:00
MananTank 26b8c320ae [TOOL-4899] Dashboard: fetch token icon from bridge api as fallback in coin asset page (#7460)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new utility function to fetch token information from a bridge and integrates it into the `ERC20PublicPage` component to enhance contract metadata with token icons.

### Detailed summary
- Added `fetchTokenInfoFromBridge` function in `fetch-coin-info.ts` to fetch token details from a bridge API.
- Integrated `fetchTokenInfoFromBridge` in the `ERC20PublicPage` component.
- Updated contract metadata to include the token icon if not already present.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Enhanced ERC20 public pages to automatically display token icons when available, enriching token metadata with external information.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 19:46:52 +00:00
Phillip HoandGitHub c7f7f48222 chore: change webhook back to object (#7461) 2025-06-28 02:18:40 +08:00
MananTank e814248d9c [TOOL-4898] Dashboard: Replace PayEmbed with BuyWidget in asset page, add tracking (#7459)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `PayEmbed` component to `BuyWidget` in the `BuyTokenEmbed` function, enhancing the functionality with error reporting and success tracking for asset purchases.

### Detailed summary
- Replaced `PayEmbed` with `BuyWidget` in `BuyTokenEmbed`.
- Added `amount`, `chain`, and `className` props to `BuyWidget`.
- Implemented `onError` and `onSuccess` callbacks for analytics reporting.
- Updated the token address prop to ensure correct type.
- Modified chart className in `contract-analytics.tsx`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Updated the buy token section to use an improved widget with enhanced error and success handling, including dynamic titles and analytics reporting.

* **Style**
  * Adjusted the chart's aspect ratio for better display across different screen sizes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 16:43:21 +00:00
MananTank d7a0613067 [TOOL-4897] Dashboard: Add Contract creator badge in Coin and NFT asset page (#7458)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the `contractCreator` property to multiple components, allowing the display of the contract creator's address in the UI. It also adds a new function, `getContractCreator`, to retrieve the contract creator's information based on the contract's role.

### Detailed summary
- Added `contractCreator` prop to `NFTPublicPageLayout`, `ContractHeaderUI`, and `ContractCreatorBadge`.
- Implemented `getContractCreator` function to fetch the contract creator using `ThirdwebContract`.
- Updated `NFTPublicPage` and `ERC20PublicPage` to call `getContractCreator` and pass the result to the UI.
- Enhanced storybook stories to include `contractCreator` in various scenarios.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Added display of contract creator information on ERC20 and NFT public pages.
  * Introduced a badge showing the contract creator's address in the contract header section.

* **Enhancements**
  * Contract headers now conditionally show the creator badge if creator data is available.
  * Storybook stories for contract headers updated to include contract creator scenarios for improved UI previews.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 16:31:14 +00:00
MananTank dbc031664d [TOOL-4867] Dashboard: Fix Coin asset page crash on "unlimited" maxClaimableSupply (#7457)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of the `totalSupply` property in the `SupplyClaimedProgress` component and related files to support the string value `"unlimited"` instead of using a constant value from `thirdweb/utils`.

### Detailed summary
- Changed `totalSupply` type from `bigint` to `bigint | "unlimited"` in `SupplyClaimedProgress`.
- Updated the condition to check for `"unlimited"` instead of `maxUint256`.
- Modified the story for `SupplyClaimedProgress` to use `"unlimited"` instead of `maxUint256`.
- Adjusted `totalSupply` handling in `TokenDropClaim` to return `"unlimited"` if `maxClaimableSupply` equals `maxUint256`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Updated the supply progress display to clearly indicate when the total supply is unlimited.
* **Refactor**
  * Improved internal handling and representation of unlimited supply for better clarity in the UI.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 16:20:18 +00:00
saminacodes 5640d48854 [Docs] Project Rebrand (#7455)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming and rebranding various components and documentation from `Universal Bridge` and `Engine` to `Payments` and `Transactions`, respectively, to better reflect their functionality and purpose.

### Detailed summary
- Renamed `Universal Bridge` to `Payments` across multiple files.
- Renamed `Engine` to `Transactions` across various components and documentation.
- Updated metadata and titles in numerous `.mdx` files to reflect the new branding.
- Changed references in UI components and layout files to match new naming conventions.
- Adjusted descriptions and summaries in documentation to align with the new terms.
- Updated links and API references to point to the correct services.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Expanded the learning resources section with updated and additional cards, including new topics like "Contracts" and "Vault".

* **Style**
  * Updated navigation and sidebar labels for improved clarity (e.g., "Connect" to "Wallets", "Bridge" to "Payments", "Engine" to "Transactions").
  * Swapped the order of the GitHub icon and theme switcher in the header for better usability.

* **Documentation**
  * Renamed all references from "Universal Bridge" to "Payments" and from "Engine" to "Transactions" across guides, FAQs, and documentation.
  * Updated instructions, headings, and content to reflect new product names and improved clarity.
  * Added a new FAQ entry explaining the difference between "Transactions Cloud" and "Dedicated" infrastructure.
  * Updated links and navigation paths to match new terminology.
  * Adjusted terminology in wallet-related documentation to replace "Connect" with "Wallets".
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 15:26:01 +00:00
GiselleNessi 9d6739ea59 updated text support page (#7447)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the descriptions and titles of various features in the support section of the dashboard, as well as changing a header for improved clarity.

### Detailed summary
- Updated `description` and `title` for the wallets feature.
- Changed `description` and `title` for the universal bridge feature, along with its `viewAllUrl`.
- Modified `description` and `title` for the engine feature, including its `viewAllUrl`.
- Revised `description` for the insight feature.
- Changed the header from "Learning Resources" to "Support Articles".

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Refactor**
  * Updated product names and descriptions for greater clarity in the Support section.
  * Changed the heading above product cards from "Learning Resources" to "Support Articles."
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 10:52:59 +00:00
GiselleNessi dc49683357 removed next steps chainlist page (#7443)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing unused components and updating product descriptions in the `page.tsx` and `products.ts` files.

### Detailed summary
- Deleted `NextSteps` and `ClaimChainSection` components from `page.tsx`.
- Removed related JSX comments in `page.tsx`.
- Updated product descriptions and names in `products.ts`:
  - Changed "Pre-built contracts & deployment tools" to "Create, deploy and manage smart contracts".
  - Changed "Client-side SDKs for wallets and blockchain interactions" to "Create and manage crypto wallets".
  - Changed "Engine" to "Transactions".
  - Changed "Account factory contracts, bundler & paymaster" to "Enable gas sponsorship for seamless transactions".
  - Changed "Point of sale solution for bridging, onramping & swapping" to "Enable payments in any token on any chain".
  - Changed "Query, transform and analyze blockchain data" to "Query and analyze blockchain data".

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Refactor**
  * Removed the "Next Steps" and "Claim Chain" sections from the chain page, including related external links and prompts.
* **Documentation**
  * Updated product names and descriptions to use clearer, more user-focused language for better understanding of features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 10:42:56 +00:00
202 changed files with 6642 additions and 5458 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"extends": "//"
}
+3 -2
View File
@@ -11,6 +11,7 @@
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
@@ -30,7 +31,7 @@
"@tanstack/react-table": "^8.21.3",
"@thirdweb-dev/service-utils": "workspace:*",
"@thirdweb-dev/vault-sdk": "workspace:*",
"@vercel/functions": "2.1.0",
"@vercel/functions": "2.2.2",
"@vercel/og": "^0.6.8",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
@@ -107,7 +108,7 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"checkly": "5.6.0",
"checkly": "6.0.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
+20 -1
View File
@@ -106,9 +106,27 @@ const projectPageRedirects = [
source: `${projectRoute}/nebula/:path*`,
},
{
source: `${projectRoute}/connect/analytics`,
destination: `${projectRoute}`,
permanent: false,
source: `${projectRoute}/connect/analytics`,
},
];
const teamPageRedirects = [
{
destination: "/team/:team_slug/~/billing/:path*",
permanent: false,
source: "/team/:team_slug/~/settings/billing/:path*",
},
{
destination: "/team/:team_slug/~/billing/invoices/:path*",
permanent: false,
source: "/team/:team_slug/~/settings/invoices/:path*",
},
{
destination: "/team/:team_slug/~/billing",
permanent: false,
source: "/team/:team_slug/~/settings/credits",
},
];
@@ -426,6 +444,7 @@ async function redirects() {
},
...legacyDashboardToTeamRedirects,
...projectPageRedirects,
...teamPageRedirects,
];
}
@@ -8,11 +8,13 @@ import {
SelectValue,
} from "@/components/ui/select";
import { normalizeTime } from "@/lib/time";
import { cn } from "@/lib/utils";
export function DateRangeSelector(props: {
range: Range;
setRange: (range: Range) => void;
popoverAlign?: "start" | "end" | "center";
className?: string;
}) {
const { range, setRange } = props;
const daysDiff = differenceInCalendarDays(range.to, range.from);
@@ -26,7 +28,7 @@ export function DateRangeSelector(props: {
return (
<DatePickerWithRange
className="w-auto bg-card"
className={cn("w-auto bg-card", props.className)}
from={range.from}
header={
<div className="mb-2 border-border border-b p-4">
@@ -76,8 +76,9 @@ export function RangeSelector({
});
return (
<div className="flex justify-end gap-4">
<div className="flex justify-end gap-3 flex-col lg:flex-row">
<DateRangeSelector
className="rounded-full"
range={localRange}
setRange={(newRange) => {
setRange(newRange);
@@ -89,7 +90,7 @@ export function RangeSelector({
}}
/>
<IntervalSelector
className="bg-card"
className="bg-card rounded-full"
intervalType={localInterval}
setIntervalType={(newInterval) => {
setInterval(newInterval);
@@ -82,7 +82,7 @@ function UnpaidInvoicesWarning({ teamSlug }: { teamSlug: string }) {
<Button asChild className="w-full gap-2 bg-card" variant="outline">
<Link
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
rel="noopener noreferrer"
target="_blank"
>
@@ -60,7 +60,7 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {
You have outstanding invoices. Please{" "}
<Link
className="font-medium text-amber-700 underline transition-colors hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300"
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
>
pay them
</Link>{" "}
@@ -44,7 +44,7 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
<div className="flex w-full flex-col gap-2">
<Button asChild className="justify-start gap-2" size="sm">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
rel="noopener noreferrer"
target="_blank"
>
@@ -53,7 +53,7 @@ export function MobileSidebar(props: {
);
}
export function useActiveSidebarLink(links: SidebarLink[]) {
function useActiveSidebarLink(links: SidebarLink[]) {
const pathname = usePathname();
const activeLink = useMemo(() => {
@@ -1,24 +1,7 @@
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSubItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
"use client";
import { cn } from "../../lib/utils";
import { AppFooter } from "../footers/app-footer";
import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
import { MobileSidebar } from "./MobileSidebar";
import { CustomSidebar, type SidebarLink } from "./Sidebar";
export function SidebarLayout(props: {
@@ -51,129 +34,3 @@ export function SidebarLayout(props: {
</div>
);
}
export function FullWidthSidebarLayout(props: {
contentSidebarLinks: SidebarLink[];
footerSidebarLinks?: SidebarLink[];
children: React.ReactNode;
className?: string;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar className="pt-2" collapsible="icon" side="left">
<SidebarContent className="p-2">
<RenderSidebarMenu links={contentSidebarLinks} />
</SidebarContent>
{footerSidebarLinks && (
<SidebarFooter className="pb-3">
<RenderSidebarMenu links={footerSidebarLinks} />
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
{/* right - content */}
<div className="flex h-full flex-grow flex-col overflow-y-auto">
<MobileSidebarTrigger
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
}
function RenderSidebarGroup(props: {
sidebarLinks: SidebarLink[];
groupName: string;
}) {
return (
<SidebarGroup className="p-0">
<SidebarMenuItem>
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
<SidebarGroupContent>
<RenderSidebarMenu links={props.sidebarLinks} />
</SidebarGroupContent>
</SidebarMenuItem>
</SidebarGroup>
);
}
function RenderSidebarMenu(props: { links: SidebarLink[] }) {
const sidebar = useSidebar();
return (
<SidebarMenu className="gap-1.5">
{props.links.map((link, idx) => {
// link
if ("href" in link) {
return (
<SidebarMenuSubItem key={link.href}>
<SidebarMenuButton asChild>
<NavLink
activeClassName="text-foreground bg-accent"
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span>{link.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuSubItem>
);
}
// separator
if ("separator" in link) {
return (
<SidebarSeparator
className="my-1"
key={`separator-${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
/>
);
}
// group
return (
<RenderSidebarGroup
groupName={link.group}
key={link.group}
sidebarLinks={link.links}
/>
);
})}
</SidebarMenu>
);
}
function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
const activeLink = useActiveSidebarLink(props.links);
return (
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator className="h-4" orientation="vertical" />
{activeLink && <span className="text-sm">{activeLink.label}</span>}
</div>
);
}
@@ -45,7 +45,7 @@ export function TeamPlanBadge(props: {
}
e.stopPropagation();
e.preventDefault();
router.push(`/team/${props.teamSlug}/~/settings/billing?showPlans=true`);
router.push(`/team/${props.teamSlug}/~/billing?showPlans=true`);
}
return (
@@ -0,0 +1,297 @@
"use client";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
import { AppFooter } from "@/components/footers/app-footer";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { NavLink } from "@/components/ui/NavLink";
import { Separator } from "@/components/ui/separator";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
type ShadcnSidebarBaseLink = {
href: string;
label: React.ReactNode;
exactMatch?: boolean;
icon?: React.FC<{ className?: string }>;
isActive?: (pathname: string) => boolean;
};
type ShadcnSidebarLink =
| ShadcnSidebarBaseLink
| {
group: string;
links: ShadcnSidebarBaseLink[];
}
| {
separator: true;
}
| {
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
links: Omit<ShadcnSidebarBaseLink, "icon">[];
};
export function FullWidthSidebarLayout(props: {
contentSidebarLinks: ShadcnSidebarLink[];
footerSidebarLinks?: ShadcnSidebarLink[];
children: React.ReactNode;
className?: string;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar className="pt-2" collapsible="icon" side="left">
<SidebarContent className="p-2">
<RenderSidebarMenu links={contentSidebarLinks} />
</SidebarContent>
{footerSidebarLinks && (
<SidebarFooter className="pb-3">
<RenderSidebarMenu links={footerSidebarLinks} />
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
{/* right - content */}
<div className="flex h-full flex-grow flex-col overflow-y-auto">
<MobileSidebarTrigger
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
}
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
const activeLink = useActiveShadcnSidebarLink(props.links);
const parentSubNav = props.links.find(
(link) =>
"subMenu" in link && link.links.some((l) => l.href === activeLink?.href),
);
return (
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator
className="h-4 bg-muted-foreground/50"
orientation="vertical"
/>
{parentSubNav && "subMenu" in parentSubNav && (
<>
<span className="text-sm">{parentSubNav.subMenu.label}</span>
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
</>
)}
{activeLink && <span className="text-sm">{activeLink.label}</span>}
</div>
);
}
function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
const pathname = usePathname();
const activeLink = useMemo(() => {
function isActive(link: ShadcnSidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
for (const link of links) {
if ("links" in link) {
for (const subLink of link.links) {
if (isActive(subLink)) {
return subLink;
}
}
} else if ("href" in link) {
if (isActive(link)) {
return link;
}
}
}
}, [links, pathname]);
return activeLink;
}
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
const pathname = usePathname();
const isSubnavActive = useMemo(() => {
function isActive(link: ShadcnSidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
return links.some(isActive);
}, [links, pathname]);
return isSubnavActive;
}
function RenderSidebarGroup(props: {
sidebarLinks: ShadcnSidebarLink[];
groupName: string;
}) {
return (
<SidebarGroup className="p-0">
<SidebarMenuItem>
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
<SidebarGroupContent>
<RenderSidebarMenu links={props.sidebarLinks} />
</SidebarGroupContent>
</SidebarMenuItem>
</SidebarGroup>
);
}
function RenderSidebarSubmenu(props: {
links: ShadcnSidebarBaseLink[];
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
}) {
const sidebar = useSidebar();
const isSubnavActive = useIsSubnavActive(props.links);
return (
<SidebarMenu>
<DynamicHeight transition="height 200ms ease">
<Collapsible className="group/collapsible" defaultOpen={isSubnavActive}>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton className="text-sm text-muted-foreground whitespace-nowrap">
{props.subMenu.icon && (
<props.subMenu.icon className="size-4" />
)}
<span>{props.subMenu.label}</span>
<ChevronDownIcon className="ml-auto group-[[data-state=open]]/collapsible:rotate-180 transition-transform" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className="mt-0.5">
<SidebarMenuSub className="pr-0 pl-2">
{props.links.map((link) => {
return (
<SidebarMenuSubItem key={link.href}>
<NavLink
activeClassName="text-foreground bg-accent "
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent hover:text-foreground px-2 py-1.5 rounded-lg w-full"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span className="whitespace-nowrap">{link.label}</span>
</NavLink>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</DynamicHeight>
</SidebarMenu>
);
}
function RenderSidebarMenu(props: { links: ShadcnSidebarLink[] }) {
const sidebar = useSidebar();
return (
<SidebarMenu className="gap-1.5">
{props.links.map((link, idx) => {
// link
if ("href" in link) {
return (
<SidebarMenuSubItem key={link.href}>
<SidebarMenuButton asChild>
<NavLink
activeClassName="text-foreground bg-accent"
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span>{link.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuSubItem>
);
}
// separator
if ("separator" in link) {
return (
<SidebarSeparator
className="my-1"
key={`separator-${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
/>
);
}
// subnav
if ("subMenu" in link) {
return (
<RenderSidebarSubmenu links={link.links} subMenu={link.subMenu} />
);
}
// group
return (
<RenderSidebarGroup
groupName={link.group}
key={link.group}
sidebarLinks={link.links}
/>
);
})}
</SidebarMenu>
);
}
@@ -48,7 +48,7 @@ export const PaginationButtons = (props: {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === 1}
onClick={() => {
setPage(activePage - 1);
@@ -57,7 +57,7 @@ export const PaginationButtons = (props: {
</PaginationItem>
<PaginationItem>
<PaginationNext
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === totalPages}
onClick={() => {
setPage(activePage + 1);
@@ -78,7 +78,7 @@ export const PaginationButtons = (props: {
{pages.map((page) => (
<PaginationItem key={page}>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
isActive={activePage === page}
onClick={() => {
setPage(page);
@@ -98,7 +98,7 @@ export const PaginationButtons = (props: {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === 1}
onClick={() => {
setPage(activePage - 1);
@@ -111,7 +111,7 @@ export const PaginationButtons = (props: {
<>
<PaginationItem>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(1);
}}
@@ -129,7 +129,7 @@ export const PaginationButtons = (props: {
{activePage - 1 > 0 && (
<PaginationItem className="max-sm:hidden">
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(activePage - 1);
}}
@@ -140,7 +140,7 @@ export const PaginationButtons = (props: {
)}
<PaginationItem>
<PaginationLink className="rounded-full" isActive>
<PaginationLink className="rounded-full lg:min-w-[38px]" isActive>
{activePage}
</PaginationLink>
</PaginationItem>
@@ -148,7 +148,7 @@ export const PaginationButtons = (props: {
{activePage + 1 <= totalPages && (
<PaginationItem className="max-sm:hidden">
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(activePage + 1);
}}
@@ -167,7 +167,7 @@ export const PaginationButtons = (props: {
<PaginationItem>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(totalPages);
}}
@@ -180,7 +180,7 @@ export const PaginationButtons = (props: {
<PaginationItem>
<PaginationNext
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === totalPages}
onClick={() => {
setPage(activePage + 1);
@@ -138,7 +138,7 @@ export function UpsellContent(props: {
<div className="flex flex-col gap-3 pt-4 sm:flex-row">
<Button asChild className="flex-1 py-3 font-semibold" size="lg">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
>
<CrownIcon className="mr-2 h-4 w-4" />
Upgrade to{" "}
@@ -146,9 +146,7 @@ export function UpsellContent(props: {
</Link>
</Button>
<Button asChild className="md:flex-1" size="lg" variant="outline">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
>
<Link href={`/team/${props.teamSlug}/~/billing?showPlans=true`}>
View All Plans
</Link>
</Button>
@@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -50,9 +50,10 @@ const PaginationLink = ({
<Button
aria-current={isActive ? "page" : undefined}
className={cn(
"h-auto px-3 py-2",
"h-auto px-3 py-2 border border-transparent",
className,
props.disabled && "cursor-not-allowed",
!isActive && "border-border",
)}
variant={!isActive ? "outline" : "default"}
{...props}
-7
View File
@@ -6,13 +6,6 @@ import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
// FIXME: We keep repeating types, API server should provide them
export const accountPlan = {
enterprise: "enterprise",
free: "free",
growth: "growth",
pro: "pro",
} as const;
export type AuthorizedWallet = {
id: string;
accountId: string;
@@ -1,37 +0,0 @@
"use client";
import { FileTextIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadata } from "thirdweb/chains";
import { SectionTitle } from "../server/SectionTitle";
export default function NextSteps(props: { chain: ChainMetadata }) {
const { chain } = props;
return (
<section>
<SectionTitle title="Next Steps" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
<div className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border">
<FileTextIcon className="mt-0.5 size-5 shrink-0" />
<div>
<h3 className="mb-1.5 font-medium">
<Link
className="before:absolute before:inset-0"
href={
"https://blog.thirdweb.com/supercharge-user-adoption-integrate-embedded-wallets-in-minutes/"
}
rel="noopener noreferrer"
target="_blank"
>
Create a login for {chain.name}
</Link>
</h3>
<p className="text-muted-foreground text-sm">
Supercharge User Adoption—Integrate In-App Wallets in Minutes
</p>
</div>
</div>
</div>
</section>
);
}
@@ -1,31 +0,0 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { SectionTitle } from "./SectionTitle";
export function ClaimChainSection() {
return (
<section>
<SectionTitle title="Claim Chain" />
<div className="rounded-lg border bg-card p-4">
<p className="mb-1"> This chain page is unclaimed </p>
<p className="mb-5 text-muted-foreground text-sm">
Are you the owner of this ecosystem? Claim this page to add additional
information on this page to help your developers get started!
</p>
<Button asChild variant="outline">
<Link
className="gap-2"
href="https://share.hsforms.com/1qAViW3U5SoKbabNhRZy4Kgea58c"
rel="noopener noreferrer"
target="_blank"
>
<span>Claim Chain</span>
<ExternalLinkIcon className="-mt-[3px] size-4" />
</Link>
</Button>
</div>
</section>
);
}
@@ -3,9 +3,7 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { getChain, getChainMetadata } from "../../utils";
import { BuyFundsSection } from "./components/client/BuyFundsSection";
import NextSteps from "./components/client/NextSteps";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ClaimChainSection } from "./components/server/ClaimChainSection";
import { ChainCTA } from "./components/server/cta-card";
import { ExplorersSection } from "./components/server/explorer-section";
import { FaucetSection } from "./components/server/FaucetSection";
@@ -60,11 +58,6 @@ export default async function Page(props: {
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}
{/*Next Steps */}
<NextSteps chain={chain} />
{/* Claim Chain */}
{!chainMetadata && <ClaimChainSection />}
</div>
);
}
@@ -0,0 +1,27 @@
import type { ThirdwebContract } from "thirdweb";
import { isOwnerSupported, owner } from "thirdweb/extensions/common";
import {
getRoleMember,
isGetRoleAdminSupported,
} from "thirdweb/extensions/permissions";
export async function getContractCreator(
contract: ThirdwebContract,
functionSelectors: string[],
) {
if (isOwnerSupported(functionSelectors)) {
return owner({
contract,
});
}
if (isGetRoleAdminSupported(functionSelectors)) {
return getRoleMember({
contract,
index: BigInt(0),
role: "admin",
});
}
return null;
}
@@ -1,6 +1,5 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { maxUint256 } from "thirdweb/utils";
import { BadgeContainer } from "../../../../../../../../@/storybook/utils";
import { BadgeContainer } from "@/storybook/utils";
import { SupplyClaimedProgress } from "./supply-claimed-progress";
const meta = {
@@ -19,7 +18,7 @@ function StoryVariants() {
return (
<div className="container max-w-md space-y-10 py-10">
<BadgeContainer label="10 / Unlimited Supply">
<SupplyClaimedProgress claimedSupply={10n} totalSupply={maxUint256} />
<SupplyClaimedProgress claimedSupply={10n} totalSupply="unlimited" />
</BadgeContainer>
<BadgeContainer label="500/1000 Supply">
@@ -1,14 +1,13 @@
import { InfinityIcon } from "lucide-react";
import { maxUint256 } from "thirdweb/utils";
import { Progress } from "@/components/ui/progress";
import { supplyFormatter } from "../nft/format";
export function SupplyClaimedProgress(props: {
claimedSupply: bigint;
totalSupply: bigint;
totalSupply: bigint | "unlimited";
}) {
// if total supply is unlimited
if (props.totalSupply === maxUint256) {
if (props.totalSupply === "unlimited") {
return (
<p className="flex items-center justify-between gap-2">
<span className="font-medium text-sm">Claimed Supply </span>
@@ -0,0 +1,19 @@
import type { ThirdwebContract } from "thirdweb";
import { WalletAddress } from "@/components/blocks/wallet-address";
export function ContractCreatorBadge(props: {
contractCreator: string;
clientContract: ThirdwebContract;
}) {
return (
<div className="flex items-center gap-1.5 bg-card rounded-full px-2.5 py-1.5 border hover:bg-accent">
<span className="text-xs text-foreground">By</span>
<WalletAddress
address={props.contractCreator}
className="py-0 text-xs h-auto !no-underline"
client={props.clientContract.client}
iconClassName="size-3.5 hidden"
/>
</div>
);
}
@@ -88,6 +88,25 @@ export const WithImageAndMultipleSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: mockTokenImage,
name: "Sample Token",
socialUrls: {
discord: mockSocialUrls.discord,
github: mockSocialUrls.github,
telegram: mockSocialUrls.telegram,
twitter: mockSocialUrls.twitter,
website: mockSocialUrls.website,
},
symbol: "SMPL",
},
};
export const WithContractCreator: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: "0x1234567890123456789012345678901234567890",
image: mockTokenImage,
name: "Sample Token",
socialUrls: {
@@ -105,6 +124,7 @@ export const WithBrokenImageAndSingleSocialUrl: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "broken-image.png",
name: "Sample Token",
socialUrls: {
@@ -118,6 +138,7 @@ export const WithoutImageAndNoSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: undefined,
name: "Sample Token",
socialUrls: {},
@@ -129,6 +150,7 @@ export const LongNameAndLotsOfSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "This is a very long token name that should wrap to multiple lines",
socialUrls: {
@@ -148,6 +170,7 @@ export const AllSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "Sample Token",
socialUrls: {
@@ -171,6 +194,7 @@ export const InvalidSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "Sample Token",
socialUrls: {
@@ -188,6 +212,7 @@ export const SomeSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "Sample Token",
socialUrls: {
@@ -19,6 +19,7 @@ import { YoutubeIcon } from "@/icons/brand-icons/YoutubeIcon";
import { ChainIconClient } from "@/icons/ChainIcon";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { ContractCreatorBadge } from "./ContractCreatorBadge";
const platformToIcons: Record<string, React.FC<{ className?: string }>> = {
discord: DiscordIcon,
@@ -42,6 +43,7 @@ export function ContractHeaderUI(props: {
clientContract: ThirdwebContract;
socialUrls: object;
imageClassName?: string;
contractCreator: string | null;
}) {
const socialUrls = useMemo(() => {
const socialUrlsValue: { name: string; href: string }[] = [];
@@ -147,6 +149,13 @@ export function ContractHeaderUI(props: {
{/* bottom row */}
<div className="flex flex-row flex-wrap items-center gap-2">
{props.contractCreator && (
<ContractCreatorBadge
clientContract={props.clientContract}
contractCreator={props.contractCreator}
/>
)}
<ToolTipLabel
contentClassName="max-w-[300px]"
label={
@@ -2,45 +2,47 @@
import { useTheme } from "next-themes";
import type { Chain, ThirdwebClient } from "thirdweb";
import { PayEmbed } from "thirdweb/react";
import { getSDKTheme } from "../../../../../../../../../@/utils/sdk-component-theme";
import { BuyWidget } from "thirdweb/react";
import {
reportAssetBuyFailed,
reportAssetBuySuccessful,
} from "@/analytics/report";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
export function BuyTokenEmbed(props: {
client: ThirdwebClient;
chain: Chain;
tokenSymbol: string;
tokenName: string;
tokenAddress: string;
}) {
const { theme } = useTheme();
return (
<PayEmbed
<BuyWidget
amount="1"
chain={props.chain}
className="!rounded-xl !w-full"
client={props.client}
connectOptions={{
autoConnect: false,
}}
payOptions={{
metadata: {
name: `Buy ${props.tokenSymbol}`,
},
mode: "fund_wallet",
prefillBuy: {
allowEdits: {
amount: true,
chain: false,
token: false,
},
amount: "1",
chain: props.chain,
token: {
address: props.tokenAddress,
name: props.tokenName,
symbol: props.tokenSymbol,
},
},
onError={(e) => {
const errorMessage = parseError(e);
reportAssetBuyFailed({
assetType: "coin",
chainId: props.chain.id,
contractType: "DropERC20",
error: errorMessage,
});
}}
onSuccess={() => {
reportAssetBuySuccessful({
assetType: "coin",
chainId: props.chain.id,
contractType: "DropERC20",
});
}}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
tokenAddress={props.tokenAddress as `0x${string}`}
/>
);
}
@@ -26,7 +26,7 @@ import {
getApprovalForTransaction,
} from "thirdweb/extensions/erc20";
import { useActiveAccount, useSendTransaction } from "thirdweb/react";
import { getClaimParams } from "thirdweb/utils";
import { getClaimParams, maxUint256 } from "thirdweb/utils";
import {
reportAssetBuyFailed,
reportAssetBuySuccessful,
@@ -328,9 +328,16 @@ export function TokenDropClaim(props: {
claimedSupply={BigInt(
toTokens(props.claimCondition.supplyClaimed, props.decimals),
)}
totalSupply={BigInt(
toTokens(props.claimCondition.maxClaimableSupply, props.decimals),
)}
totalSupply={
props.claimCondition.maxClaimableSupply === maxUint256
? "unlimited"
: BigInt(
toTokens(
props.claimCondition.maxClaimableSupply,
props.decimals,
),
)
}
/>
<div className="h-4" />
@@ -39,7 +39,7 @@ export function ContractAnalyticsOverview(props: {
</div>
<ThirdwebAreaChart
cardContentClassName="p-0"
chartClassName="aspect-[1.5] lg:aspect-[3]"
chartClassName="aspect-[1.2] lg:aspect-[2.6]"
className="border-none bg-background p-0"
config={{
events: {
@@ -0,0 +1,32 @@
import { isProd } from "@/constants/env-utils";
export async function fetchTokenInfoFromBridge(params: {
chainId: number;
tokenAddress: string;
clientId: string;
}) {
try {
const res = await fetch(
`https://bridge.${isProd ? "thirdweb.com" : "thirdweb-dev.com"}/v1/tokens?chainId=${params.chainId}&tokenAddress=${params.tokenAddress}&clientId=${params.clientId}`,
);
if (!res.ok) {
return null;
}
const data = (await res.json()) as {
data: Array<{
iconUri: string;
address: string;
decimals: number;
name: string;
symbol: string;
priceUsd: number;
}>;
};
return data.data[0];
} catch {
return null;
}
}
@@ -2,6 +2,8 @@ import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { getContractMetadata } from "thirdweb/extensions/common";
import { decimals, getActiveClaimCondition } from "thirdweb/extensions/erc20";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { getContractCreator } from "../_components/getContractCreator";
import { PageHeader } from "../_components/PageHeader";
import { ContractHeaderUI } from "./_components/ContractHeader";
import { TokenDropClaim } from "./_components/claim-tokens/claim-tokens-ui";
@@ -9,6 +11,7 @@ import { ContractAnalyticsOverview } from "./_components/contract-analytics/cont
import { BuyTokenEmbed } from "./_components/PayEmbedSection";
import { TokenStats } from "./_components/PriceChart";
import { RecentTransfers } from "./_components/RecentTransfers";
import { fetchTokenInfoFromBridge } from "./_utils/fetch-coin-info";
import { getCurrencyMeta } from "./_utils/getCurrencyMeta";
export async function ERC20PublicPage(props: {
@@ -16,25 +19,43 @@ export async function ERC20PublicPage(props: {
clientContract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const [contractMetadata, activeClaimCondition, tokenDecimals] =
await Promise.all([
getContractMetadata({
contract: props.serverContract,
}),
getActiveClaimConditionWithErrorHandler(props.serverContract),
decimals({
contract: props.serverContract,
}),
]);
const [
contractMetadata,
activeClaimCondition,
tokenDecimals,
tokenInfo,
functionSelectors,
] = await Promise.all([
getContractMetadata({
contract: props.serverContract,
}),
getActiveClaimConditionWithErrorHandler(props.serverContract),
decimals({
contract: props.serverContract,
}),
fetchTokenInfoFromBridge({
chainId: props.serverContract.chain.id,
clientId: props.clientContract.client.clientId,
tokenAddress: props.serverContract.address,
}),
resolveFunctionSelectors(props.serverContract),
]);
const claimConditionCurrencyMeta = activeClaimCondition
? await getCurrencyMeta({
chain: props.serverContract.chain,
chainMetadata: props.chainMetadata,
client: props.serverContract.client,
currencyAddress: activeClaimCondition.currency,
}).catch(() => undefined)
: undefined;
if (!contractMetadata.image && tokenInfo) {
contractMetadata.image = tokenInfo.iconUri;
}
const [contractCreator, claimConditionCurrencyMeta] = await Promise.all([
getContractCreator(props.serverContract, functionSelectors),
activeClaimCondition
? getCurrencyMeta({
chain: props.serverContract.chain,
chainMetadata: props.chainMetadata,
client: props.serverContract.client,
currencyAddress: activeClaimCondition.currency,
}).catch(() => undefined)
: undefined,
]);
const buyEmbed = (
<BuyEmbed
@@ -62,6 +83,7 @@ export async function ERC20PublicPage(props: {
<ContractHeaderUI
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
image={contractMetadata.image}
name={contractMetadata.name}
socialUrls={
@@ -141,8 +163,6 @@ function BuyEmbed(props: {
chain={props.clientContract.chain}
client={props.clientContract.client}
tokenAddress={props.clientContract.address}
tokenName={props.tokenName}
tokenSymbol={props.tokenSymbol}
/>
);
}
@@ -12,6 +12,7 @@ export function NFTPublicPageLayout(props: {
[key: string]: unknown;
};
children: React.ReactNode;
contractCreator: string | null;
}) {
return (
<div className="flex grow flex-col">
@@ -20,6 +21,7 @@ export function NFTPublicPageLayout(props: {
<ContractHeaderUI
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={props.contractCreator}
image={
typeof props.contractMetadata.image === "string"
? props.contractMetadata.image
@@ -6,6 +6,7 @@ import { ResponsiveLayout } from "@/components/blocks/Responsive";
import { Skeleton } from "@/components/ui/skeleton";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { cn } from "@/lib/utils";
import { getContractCreator } from "../_components/getContractCreator";
import { NFTPublicPageLayout } from "./nft-page-layout";
import {
BuyNFTDropCardServer,
@@ -35,15 +36,17 @@ export async function NFTPublicPage(props: {
}),
]);
const nftDropClaimParams =
const [contractCreator, nftDropClaimParams] = await Promise.all([
getContractCreator(props.serverContract, functionSelectors),
props.type === "erc721"
? await getNFTDropClaimParams({
? getNFTDropClaimParams({
chainMetadata: props.chainMetadata,
functionSelectors,
serverContract: props.serverContract,
totalNFTCount,
})
: undefined;
: undefined,
]);
const _isTokenByIndexSupported =
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);
@@ -101,6 +104,7 @@ export async function NFTPublicPage(props: {
<NFTPublicPageLayout
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
contractMetadata={contractMetadata}
>
<ResponsiveLayout
@@ -10,18 +10,18 @@ import type { ChainSupportedService } from "@/types/chain";
export const products = [
{
description: "Pre-built contracts & deployment tools",
description: "Create, deploy and manage smart contracts",
icon: ContractIcon,
id: "contracts",
link: "https://thirdweb.com/explore",
name: "Contracts",
},
{
description: "Client-side SDKs for wallets and blockchain interactions",
description: "Create and manage crypto wallets",
icon: ConnectSDKIcon,
id: "connect-sdk",
link: "https://thirdweb.com/connect",
name: "Connect SDK",
name: "Wallets",
},
{
description: "Performant and scalable RPC service",
@@ -35,24 +35,24 @@ export const products = [
icon: EngineIcon,
id: "engine",
link: "https://thirdweb.com/engine",
name: "Engine",
name: "Transactions",
},
{
description: "Account factory contracts, bundler & paymaster",
description: "Enable gas sponsorship for seamless transactions",
icon: SmartAccountIcon,
id: "account-abstraction",
link: "https://portal.thirdweb.com/connect/account-abstraction/overview",
name: "Account Abstraction",
},
{
description: "Point of sale solution for bridging, onramping & swapping",
description: "Enable payments in any token on any chain",
icon: PayIcon,
id: "pay",
link: "https://portal.thirdweb.com/connect/pay/overview",
name: "Universal Bridge",
name: "Payments",
},
{
description: "Query, transform and analyze blockchain data",
description: "Query and analyze blockchain data",
icon: InsightIcon,
id: "insight",
link: "https://thirdweb.com/insight",
@@ -33,25 +33,25 @@ const HELP_PRODUCTS = [
"https://playground.thirdweb.com/connect/sign-in/button?tab=code",
},
{
description: "Wallets, auth, and onchain interactions",
description: "Create and manage crypto wallets",
icon: WalletIcon,
title: "Connect",
title: "Wallets",
viewAllUrl: "https://portal.thirdweb.com/connect",
},
{
description: "Bridge and onramp tokens on any chain",
description: "Enable payments on any tokens on any chain",
icon: PayIcon,
title: "Universal Bridge",
viewAllUrl: "https://portal.thirdweb.com/pay",
title: "Payments",
viewAllUrl: "https://portal.thirdweb.com/pay/troubleshoot",
},
{
description: "Reliable transactions and monitoring",
description: "Perform read and write transactions onchain",
icon: EngineIcon,
title: "Engine",
viewAllUrl: "https://portal.thirdweb.com/engine/v3",
title: "Transactions",
viewAllUrl: "https://portal.thirdweb.com/engine/v3/troubleshoot",
},
{
description: "Blockchain data queries and transformations",
description: "Query and analyze blockchain data",
icon: InsightIcon,
title: "Insight",
viewAllUrl: "https://portal.thirdweb.com/insight",
@@ -117,7 +117,7 @@ export default async function SupportPage() {
</header>
</div>
<section className="container flex flex-col gap-6">
<h2 className="font-bold text-3xl">Learning Resources</h2>
<h2 className="font-bold text-3xl">Support Articles</h2>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3">
{HELP_PRODUCTS.map((product) => (
<Card className="relative col-span-1 bg-card" key={product.title}>
@@ -76,7 +76,7 @@ export function AccountButton(props: {
className="justify-start px-3 text-muted-foreground text-sm hover:text-foreground"
variant="ghost"
>
<Link href="/team/~/~/settings/billing">Manage Billing</Link>
<Link href="/team/~/~/billing">Manage Billing</Link>
</Button>
<Button
@@ -0,0 +1,92 @@
"use client";
import {
BookTextIcon,
BoxIcon,
ChartNoAxesColumnIcon,
DatabaseIcon,
DollarSignIcon,
FileTextIcon,
HomeIcon,
SettingsIcon,
WalletCardsIcon,
} from "lucide-react";
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
export function TeamSidebarLayout(props: {
layoutPath: string;
children: React.ReactNode;
ecosystems: Array<{ name: string; slug: string }>;
}) {
const { layoutPath, children } = props;
return (
<FullWidthSidebarLayout
contentSidebarLinks={[
{
exactMatch: true,
href: layoutPath,
icon: HomeIcon,
label: "Overview",
},
{
href: `${layoutPath}/~/analytics`,
icon: ChartNoAxesColumnIcon,
label: "Analytics",
},
{
links: [
...props.ecosystems.map((ecosystem) => ({
href: `${layoutPath}/~/ecosystem/${ecosystem.slug}`,
label: ecosystem.name,
})),
{
href: `${layoutPath}/~/ecosystem/create`,
label: "Create Ecosystem",
},
],
subMenu: {
icon: WalletCardsIcon,
label: "Ecosystems",
},
},
{
href: `${layoutPath}/~/audit-log`,
icon: FileTextIcon,
label: "Audit Log",
},
{
href: `${layoutPath}/~/usage`,
icon: DatabaseIcon,
label: "Usage",
},
]}
footerSidebarLinks={[
{
href: `${layoutPath}/~/billing`,
icon: DollarSignIcon,
label: "Billing",
},
{
href: `${layoutPath}/~/settings`,
icon: SettingsIcon,
label: "Team Settings",
},
{
separator: true,
},
{
href: "https://portal.thirdweb.com",
icon: BookTextIcon,
label: "Documentation",
},
{
href: "https://playground.thirdweb.com/connect/sign-in/button",
icon: BoxIcon,
label: "Playground",
},
]}
>
{children}
</FullWidthSidebarLayout>
);
}
@@ -54,7 +54,7 @@ function BillingAlertBanner(props: {
"border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800",
)}
>
<Link href={`/team/${props.teamSlug}/~/settings/invoices`}>
<Link href={`/team/${props.teamSlug}/~/billing/invoices`}>
{props.ctaLabel}
</Link>
</Button>
@@ -18,7 +18,7 @@ export function FreePlanUpsellBannerUI(props: {
accentColor="green"
cta={{
icon: <ArrowRightIcon className="size-4" />,
link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${
link: `/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${
props.highlightPlan || "growth"
}`,
text: "View plans",
@@ -0,0 +1,29 @@
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function StaffModeNotice() {
return (
<div className="dark:bg-orange-800 bg-orange-200 border-b px-5">
<div className="flex items-center justify-between py-3">
<div className="flex flex-col">
<p className="font-bold text-lg tracking-tight">Staff Mode</p>
<p className="text-sm text-foreground/80">
You can only view this team, not take any actions.
</p>
</div>
<Button
asChild
className="rounded-full gap-1.5"
size="sm"
variant="default"
>
<Link href="/team/~">
Leave Staff Mode
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
</div>
);
}
@@ -1,17 +1,18 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { getProjects } from "@/api/projects";
import { getTeamBySlug, getTeams } from "@/api/team";
import { CustomChatButton } from "@/components/chat/CustomChatButton";
import { AppFooter } from "@/components/footers/app-footer";
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
import { Button } from "@/components/ui/button";
import { TabPathLinks } from "@/components/ui/tabs";
import { SidebarProvider } from "@/components/ui/sidebar";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { siwaExamplePrompts } from "../../../(dashboard)/support/definitions";
import { getValidAccount } from "../../../account/settings/getAccount";
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
import { StaffModeNotice } from "./_components/StaffModeNotice";
import type { Ecosystem } from "./~/ecosystem/types";
import { TeamSidebarLayout } from "./TeamSidebarLayout";
export default async function TeamLayout(props: {
children: React.ReactNode;
@@ -43,81 +44,66 @@ export default async function TeamLayout(props: {
teamId: team.id,
});
const ecosystems = await fetchEcosystemList(team.id, authToken);
const isStaffMode = !teams.some((t) => t.slug === team.slug);
return (
<div className="flex h-full grow flex-col">
{!teams.some((t) => t.slug === team.slug) && (
<div className="bg-warning-text">
<div className="container flex items-center justify-between py-4">
<div className="flex flex-col gap-2">
<p className="font-bold text-white text-xl">👀 STAFF MODE 👀</p>
<p className="text-sm text-white">
You can only view this team, not take any actions.
</p>
</div>
<Button asChild variant="default">
<Link href="/team/~">Leave Staff Mode</Link>
</Button>
</div>
<SidebarProvider>
<div className="flex h-dvh min-w-0 grow flex-col">
{isStaffMode && <StaffModeNotice />}
<AnnouncementBanner />
<div className="bg-card border-b">
<TeamHeaderLoggedIn
account={account}
accountAddress={accountAddress}
client={client}
currentProject={undefined}
currentTeam={team}
teamsAndProjects={teamsAndProjects}
/>
</div>
)}
<AnnouncementBanner />
<div className="bg-card">
<TeamHeaderLoggedIn
account={account}
accountAddress={accountAddress}
client={client}
currentProject={undefined}
currentTeam={team}
teamsAndProjects={teamsAndProjects}
/>
<TabPathLinks
links={[
{
exactMatch: true,
name: "Overview",
path: `/team/${params.team_slug}`,
},
{
name: "Analytics",
path: `/team/${params.team_slug}/~/analytics`,
},
{
name: "Ecosystems",
path: `/team/${params.team_slug}/~/ecosystem`,
},
{
name: "Usage",
path: `/team/${params.team_slug}/~/usage`,
},
{
name: "Audit Log",
path: `/team/${params.team_slug}/~/audit-log`,
},
{
name: "Settings",
path: `/team/${params.team_slug}/~/settings`,
},
]}
tabContainerClassName="px-4 lg:px-6"
/>
<TeamSidebarLayout
ecosystems={ecosystems.map((ecosystem) => ({
name: ecosystem.name,
slug: ecosystem.slug,
}))}
layoutPath={`/team/${params.team_slug}`}
>
{props.children}
</TeamSidebarLayout>
<div className="fixed right-6 bottom-6 z-50">
<CustomChatButton
authToken={authToken}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
isFloating={true}
isLoggedIn={true}
label="Ask AI Assistant"
networks="all"
pageType="support"
teamId={team.id}
/>
</div>
</div>
<main className="flex grow flex-col">{props.children}</main>
<div className="fixed right-6 bottom-6 z-50">
<CustomChatButton
authToken={authToken}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
isFloating={true}
isLoggedIn={true}
label="Ask AI Assistant"
networks="all"
pageType="support"
teamId={team.id}
/>
</div>
<AppFooter />
</div>
</SidebarProvider>
);
}
async function fetchEcosystemList(teamId: string, authToken: string) {
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/ecosystem-wallet`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
if (!res.ok) {
return [];
}
return (await res.json()).result as Ecosystem[];
}
@@ -73,7 +73,7 @@ export default async function TeamOverviewPage(props: {
title="Analytics"
/>
</div>
<div className="flex grow flex-col justify-between gap-10 md:container md:pt-8 md:pb-16">
<div className="flex grow flex-col justify-between gap-10 md:container md:max-w-7xl md:pt-8 md:pb-16">
<div className="flex grow flex-col gap-6">
<Suspense
fallback={<LoadingChartState className="h-[458px] border" />}
@@ -24,13 +24,15 @@ export default async function Layout(props: {
>
<div className="flex grow flex-col">
<div className="border-border border-b py-10">
<div className="container flex flex-row justify-between">
<div className="container max-w-5xl flex flex-row justify-between">
<h1 className="font-semibold text-3xl tracking-tight lg:px-2">
Audit Log
</h1>
</div>
</div>
{props.children}
<div className="container max-w-5xl flex flex-col gap-8 pt-6 pb-10">
{props.children}
</div>
</div>
</UpsellWrapper>
);
@@ -47,7 +47,7 @@ export default async function Page(props: {
}
return (
<div className="container flex flex-col gap-8">
<div className="flex flex-col gap-8">
{auditLogs.data.result.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
<p className="text-sm">No audit events found</p>
@@ -237,7 +237,7 @@ export function PlanInfoCardUI(props: {
size="sm"
variant="outline"
>
<Link href={`/team/${team.slug}/~/settings/invoices`}>
<Link href={`/team/${team.slug}/~/billing/invoices`}>
<FileTextIcon className="size-4 text-muted-foreground" />
View Invoices
</Link>
@@ -16,7 +16,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
import { ThirdwebMiniLogo } from "../../../../../../components/ThirdwebMiniLogo";
const predefinedAmounts = [
{ label: "$25", value: "25" },
@@ -19,7 +19,7 @@ export default async function Page(props: {
searchParamLoader(props.searchParams),
]);
const pagePath = `/team/${params.team_slug}/settings/invoices`;
const pagePath = `/team/${params.team_slug}/invoices`;
const account = await getValidAccount(pagePath);
@@ -0,0 +1,56 @@
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { TabPathLinks } from "../../../../../../../@/components/ui/tabs";
import { loginRedirect } from "../../../../../login/loginRedirect";
export default async function Layout(props: {
params: Promise<{
team_slug: string;
}>;
children: React.ReactNode;
}) {
const params = await props.params;
const [team, authToken] = await Promise.all([
getTeamBySlug(params.team_slug),
getAuthToken(),
]);
if (!authToken) {
loginRedirect(`/team/${params.team_slug}/~/settings`);
}
if (!team) {
redirect("/team");
}
return (
<div className="flex grow flex-col">
<div className="border-border pt-10 pb-6">
<div className="container max-w-6xl">
<h1 className="font-semibold text-3xl tracking-tight">Billing</h1>
</div>
</div>
<TabPathLinks
links={[
{
exactMatch: true,
name: "Overview",
path: `/team/${params.team_slug}/~/billing`,
},
{
name: "Invoices",
path: `/team/${params.team_slug}/~/billing/invoices`,
},
]}
scrollableClassName="container max-w-6xl"
/>
<div className="container max-w-6xl flex flex-col grow gap-8 pt-6 pb-10">
{props.children}
</div>
</div>
);
}
@@ -7,7 +7,7 @@ import { getTeamSubscriptions } from "@/api/team-subscription";
import { CreditsInfoCard } from "@/components/billing/PlanCard";
import { Coupons } from "@/components/billing/SubscriptionCoupons/Coupons";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getValidAccount } from "../../../../../../account/settings/getAccount";
import { getValidAccount } from "../../../../../account/settings/getAccount";
import { CreditBalanceSection } from "./components/credit-balance-section.client";
import { PlanInfoCardClient } from "./components/PlanInfoCard.client";
@@ -24,7 +24,7 @@ export default async function Page(props: {
props.params,
props.searchParams,
]);
const pagePath = `/team/${params.team_slug}/settings/billing`;
const pagePath = `/team/${params.team_slug}/billing`;
const account = await getValidAccount(pagePath);
@@ -64,7 +64,7 @@ export default async function Page(props: {
team.billingStatus === "validPayment" || team.billingStatus === "pastDue";
return (
<div className="flex flex-col gap-12">
<div className="flex flex-col gap-8">
<div>
<PlanInfoCardClient
highlightPlan={highlightPlan}
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { TabPathLinks } from "@/components/ui/tabs";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../../../../../../@/api/auth-token";
import { fetchEcosystem } from "../../../utils/fetchEcosystem";
import { EcosystemHeader } from "./ecosystem-header.client";
@@ -54,25 +54,28 @@ export async function EcosystemLayoutSlug({
teamIdOrSlug={params.team_slug}
/>
<SidebarLayout
sidebarLinks={[
<TabPathLinks
className="pt-2"
links={[
{
exactMatch: true,
href: `${ecosystemLayoutPath}/${ecosystem.slug}`,
label: "Overview",
name: "Overview",
path: `${ecosystemLayoutPath}/${ecosystem.slug}`,
},
{
href: `${ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
label: "Analytics",
name: "Analytics",
path: `${ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
},
{
href: `${ecosystemLayoutPath}/${ecosystem.slug}#`,
label: "Design (coming soon)",
name: "Design (coming soon)",
path: `${ecosystemLayoutPath}/${ecosystem.slug}/#`,
},
]}
>
scrollableClassName="container max-w-7xl"
/>
<div className="container max-w-7xl flex flex-col grow pt-6 pb-10">
{children}
</SidebarLayout>
</div>
</div>
);
}
@@ -1,12 +1,5 @@
"use client";
import {
AlertTriangleIcon,
CheckIcon,
ChevronsUpDownIcon,
ExternalLinkIcon,
PencilIcon,
PlusCircleIcon,
} from "lucide-react";
import { AlertTriangleIcon, ExternalLinkIcon, PencilIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
@@ -25,14 +18,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/Spinner/Spinner";
@@ -41,7 +26,6 @@ import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
import type { Ecosystem } from "../../../types";
import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem";
import { useEcosystem } from "../hooks/use-ecosystem";
@@ -78,56 +62,6 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
}
}
function EcosystemSelect(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
}) {
const { data: ecosystems, isPending } = useEcosystemList({
teamIdOrSlug: props.teamIdOrSlug,
});
return isPending ? (
<Skeleton className="h-10 w-full md:w-[160px]" />
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="relative flex w-full justify-start truncate pr-8 pl-3 md:w-48"
variant="outline"
>
<div className="truncate">{props.ecosystem?.name}</div>
<ChevronsUpDownIcon className="absolute right-2 h-4 w-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full md:w-48">
<DropdownMenuGroup>
{ecosystems?.map((ecosystem) => (
<DropdownMenuItem asChild key={ecosystem.id}>
<Link
className="relative flex cursor-pointer items-center pr-3 pl-8"
href={`${props.ecosystemLayoutPath}/${ecosystem.slug}`}
>
{ecosystem.slug === props.ecosystem.slug && (
<CheckIcon className="absolute left-2 h-4 w-4 text-foreground" />
)}
<div className="truncate">{ecosystem.name}</div>
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<Link className="" href={`${props.ecosystemLayoutPath}/create`}>
<DropdownMenuItem className="relative flex cursor-pointer items-center pr-3 pl-8">
<PlusCircleIcon className="absolute left-2 h-4 w-4" />
<div className="truncate">New Ecosystem</div>
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
);
}
export function EcosystemHeader(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
@@ -236,8 +170,8 @@ export function EcosystemHeader(props: {
}
return (
<div className="border-b py-8">
<div className="container flex flex-col gap-8">
<div className="pt-8 pb-4">
<div className="container max-w-7xl flex flex-col gap-8">
<EcosystemAlertBanner ecosystem={ecosystem} />
<header className="flex flex-col gap-12">
<div className="flex flex-col justify-between gap-4 md:grid-cols-4 md:flex-row">
@@ -402,13 +336,6 @@ export function EcosystemHeader(props: {
)}
</div>
</div>
<div className="flex flex-col justify-between gap-4 md:items-end">
<EcosystemSelect
ecosystem={ecosystem}
ecosystemLayoutPath={props.ecosystemLayoutPath}
teamIdOrSlug={props.teamIdOrSlug}
/>
</div>
</div>
</header>
</div>
@@ -1,3 +1,4 @@
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { CreateEcosystemForm } from "./components/client/create-ecosystem-form.client";
import { EcosystemWalletPricingCard } from "./components/pricing-card";
@@ -6,8 +7,29 @@ export async function EcosystemCreatePage(props: {
teamId: string;
}) {
return (
<div className="flex grow flex-col items-center justify-center">
<main className="container max-w-5xl py-20">
<div>
<div className="border-border py-8 border-b">
<div className="container max-w-5xl">
<h1 className="font-semibold text-3xl tracking-tight mb-1">
Create Ecosystem
</h1>
<p className="text-muted-foreground">
Ecosystem Wallets enable your users to seamlessly access their
assets across various apps and games within your ecosystem.
<br /> You can control which apps join your ecosystem and how their
users interact with your wallet.{" "}
<UnderlineLink
href="https://portal.thirdweb.com/connect/ecosystems/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</UnderlineLink>
</p>
</div>
</div>
<div className="flex flex-col grow container max-w-5xl pb-20 pt-8">
<div className="flex flex-col rounded-lg border bg-card lg:flex-row">
<CreateEcosystemForm
teamId={props.teamId}
@@ -15,7 +37,7 @@ export async function EcosystemCreatePage(props: {
/>
<EcosystemWalletPricingCard />
</div>
</main>
</div>
</div>
);
}
@@ -1,7 +1,6 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -20,6 +19,7 @@ import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { createEcosystem } from "../../actions/create-ecosystem";
const formSchema = z.object({
@@ -87,15 +87,6 @@ export function CreateEcosystemForm(props: {
})}
>
<div className="grid gap-6 p-4 lg:p-6">
<div>
<h2 className="font-semibold text-2xl tracking-tight">
Create Ecosystem
</h2>
<p className="text-muted-foreground">
Create wallets that work across every chain and every app
</p>
</div>
<FormField
control={form.control}
name="name"
@@ -175,14 +166,13 @@ export function CreateEcosystemForm(props: {
</RadioGroup>
</FormControl>
<FormDescription>
<Link
className="text-primary"
href="https://portal.thirdweb.com/connect/ecosystems/ecosystem-permissions"
<UnderlineLink
href="https://portal.thirdweb.com/connect/wallet/ecosystem/permissions"
rel="noopener noreferrer"
target="_blank"
>
Learn more about ecosystem permissions
</Link>
</UnderlineLink>
</FormDescription>
<FormMessage />
</FormItem>
@@ -1,3 +1,7 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
export default function Loading() {
return <GenericLoadingPage className="border-none" />;
}
@@ -1,25 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useActiveAccount } from "thirdweb/react";
import { apiServerProxy } from "@/actions/proxies";
import type { Ecosystem } from "../types";
export function useEcosystemList({ teamIdOrSlug }: { teamIdOrSlug: string }) {
const address = useActiveAccount()?.address;
return useQuery({
queryFn: async () => {
const res = await apiServerProxy({
method: "GET",
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
});
if (!res.ok) {
throw new Error(res.error ?? "Failed to fetch ecosystems");
}
const data = res.data as { result: Ecosystem[] };
return data.result;
},
queryKey: ["ecosystems", teamIdOrSlug, address],
retry: false,
});
}
@@ -1,11 +1,6 @@
import { ArrowRightIcon, ExternalLinkIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
import { Button } from "@/components/ui/button";
import { getAuthToken } from "../../../../../../../@/api/auth-token";
import { getAuthToken } from "@/api/auth-token";
import { loginRedirect } from "../../../../../login/loginRedirect";
import headerImage from "./assets/header.png";
import { fetchEcosystemList } from "./utils/fetchEcosystemList";
export default async function Page(props: {
@@ -26,61 +21,10 @@ export default async function Page(props: {
return [];
},
);
if (ecosystems[0]) {
redirect(`${ecosystemLayoutPath}/${ecosystems[0].slug}`);
}
return <EcosystemLandingPage ecosystemLayoutPath={ecosystemLayoutPath} />;
}
async function EcosystemLandingPage(props: { ecosystemLayoutPath: string }) {
return (
<div className="container flex grow flex-col items-center justify-center py-20">
{/* Card */}
<div className="flex max-w-lg flex-col rounded-lg border bg-card">
<Image
alt="Ecosystems"
className="w-full border-b"
sizes="100vw"
src={headerImage}
/>
{/* body */}
<div className="px-4 py-6 lg:px-6">
<h2 className="mb-2 text-balance font-semibold text-2xl text-foreground leading-tight tracking-tighter">
One wallet, a whole ecosystem of apps and games
</h2>
<p className="text-muted-foreground">
With Ecosystem Wallets, your users can access their assets across
hundreds of apps and games within your ecosystem. You can control
which apps join your ecosystem and how their users interact with
your wallet.
</p>
</div>
{/* Footer */}
<div className="mt-6 flex flex-col justify-end gap-4 border-t px-4 py-6 lg:flex-row lg:px-6">
<Button asChild variant="outline">
<Link
className="flex flex-row gap-2"
href="https://portal.thirdweb.com/connect/ecosystems/overview"
rel="noopener noreferrer"
target="_blank"
>
Documentation
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
<Button asChild className="gap-2">
<Link href={`${props.ecosystemLayoutPath}/create`}>
Create Ecosystem
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
</div>
</div>
);
redirect(`${ecosystemLayoutPath}/create`);
}
@@ -1,68 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
import { useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { Team } from "@/api/team";
import type { Account } from "@/hooks/useApi";
import { cn } from "@/lib/utils";
import { getTeamSettingsLinks } from "./_components/sidebar/getTeamSettingsLinks";
import { TeamSettingsSidebar } from "./_components/sidebar/TeamSettingsSidebar";
import { TeamSettingsMobileNav } from "./_components/sidebar/TeamsMobileNav";
// on the /~/settings page
// - On desktop: show the general settings as usual
// - On mobile: show the full nav instead of page content and when user clicks on the "General Settings" ( first link ) - hide the full nav and show the page content
export function SettingsLayout(props: {
team: Team;
children: React.ReactNode;
account: Account;
client: ThirdwebClient;
}) {
const [_showFullNavOnMobile, setShowFullNavOnMobile] = useState(true);
const pathname = usePathname();
const isSettingsOverview = (pathname || "").endsWith("/~/settings");
const showFullNavOnMobile = _showFullNavOnMobile && isSettingsOverview;
const links = getTeamSettingsLinks(props.team.slug);
const activeLink = links.find((link) => pathname === link.href);
return (
<div className="flex grow flex-col">
{/* Huge page title */}
<div className="border-border border-b py-10">
<div className="container">
<h1 className="font-semibold text-3xl tracking-tight">
Team Settings
</h1>
</div>
</div>
<div className="md:hidden">
<TeamSettingsMobileNav
activeLink={activeLink}
setShowFull={setShowFullNavOnMobile}
showFull={showFullNavOnMobile}
teamSlug={props.team.slug}
/>
</div>
<div className="container flex grow gap-8 lg:min-h-[900px] [&>*]:py-8 lg:[&>*]:py-10">
<TeamSettingsSidebar
account={props.account}
client={props.client}
team={props.team}
/>
<div
className={cn(
"flex max-w-full grow flex-col",
// if showing full nav on mobile - hide the page content
showFullNavOnMobile && "max-sm:hidden",
)}
>
{props.children}
</div>
</div>
</div>
);
}
@@ -131,7 +131,7 @@ export function TeamDedicatedSupportCard(props: {
label: "Upgrade Plan",
onClick: () =>
router.push(
`/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=scale`,
`/team/${props.team.slug}/~/billing?showPlans=true&highlight=scale`,
),
}
}
@@ -13,7 +13,7 @@ export function TeamSettingsMobileNav(props: {
if (!showFull) {
return (
<div className="flex items-center gap-2 border-border border-b px-4 py-4 text-muted-foreground">
<div className="flex items-center gap-2 border-border border-b px-4 py-4 text-muted-foreground text-sm">
<Link
className="inline-flex items-center gap-1"
href={teamLinks[0]?.href || "#"}
@@ -1,35 +0,0 @@
"use client";
import { ApplyForOpCredits } from "@app/team/[team_slug]/(team)/~/settings/credits/components/ApplyForOpCreditsModal";
import type { Team } from "@/api/team";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import type { Account } from "@/hooks/useApi";
export const SettingsGasCreditsPage = (props: {
team: Team;
account: Account;
}) => {
return (
<div>
<div className="flex flex-row items-center justify-between gap-4">
<div>
<h2 className="mb-0.5 font-semibold text-2xl tracking-tight">
Credits
</h2>
<p className="text-muted-foreground text-sm">
Apply to the Optimism Superchain App Accelerator.{" "}
<UnderlineLink
href="https://blog.thirdweb.com/accelerating-the-superchain-with-optimism/"
rel="noopener noreferrer"
target="_blank"
>
Learn More
</UnderlineLink>
</p>
</div>
</div>
<div className="h-6" />
<ApplyForOpCredits account={props.account} team={props.team} />
</div>
);
};
@@ -1,216 +0,0 @@
import { Flex, FormControl } from "@chakra-ui/react";
import { FormHelperText, FormLabel } from "chakra/form";
import { Select as ChakraSelect } from "chakra-react-select";
import { useMemo } from "react";
import { useForm } from "react-hook-form";
import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import type { Account } from "@/hooks/useApi";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { useTxNotifications } from "@/hooks/useTxNotifications";
import { PlanToCreditsRecord } from "./ApplyForOpCreditsModal";
import { applyOpSponsorship } from "./applyOpSponsorship";
interface FormSchema {
firstname: string;
lastname: string;
thirdweb_account_id: string;
plan_type: string;
email: string;
company: string;
website: string;
twitterhandle: string;
superchain_verticals: string;
superchain_chain: string;
what_would_you_like_to_meet_about_: string;
}
interface ApplyForOpCreditsFormProps {
onClose: () => void;
plan: Team["billingPlan"];
account: Account;
}
export const ApplyForOpCreditsForm: React.FC<ApplyForOpCreditsFormProps> = ({
onClose,
account,
plan,
}) => {
const [, setHasAppliedForOpGrant] = useLocalStorage(
`appliedForOpGrant-${account?.id}`,
false,
);
const transformedQueryData = useMemo(
() => ({
company: "",
email: account?.email || "",
firstname: "",
lastname: "",
plan_type: PlanToCreditsRecord[plan].plan,
superchain_chain: "",
superchain_verticals: "",
thirdweb_account_id: account?.id || "",
twitterhandle: "",
website: "",
what_would_you_like_to_meet_about_: "",
}),
[account, plan],
);
const form = useForm<FormSchema>({
defaultValues: transformedQueryData,
values: transformedQueryData,
});
const { onSuccess, onError } = useTxNotifications(
"We have received your application and will notify you if you are selected.",
"Something went wrong, please try again.",
);
return (
<Flex
as="form"
direction="column"
gap={4}
onSubmit={form.handleSubmit(async (data) => {
const fields = Object.keys(data).map((key) => ({
name: key,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
value: (data as any)[key],
}));
try {
const response = await applyOpSponsorship({
fields,
});
if (!response.ok) {
throw new Error("Form submission failed");
}
onSuccess();
onClose();
setHasAppliedForOpGrant(true);
form.reset();
} catch (error) {
onError(error);
}
})}
>
<Flex flexDir="column" gap={4}>
<Flex gap={4}>
<FormControl gap={6} isRequired>
<FormLabel>First Name</FormLabel>
<Input {...form.register("firstname", { required: true })} />
</FormControl>
<FormControl gap={6} isRequired>
<FormLabel>Last Name</FormLabel>
<Input {...form.register("lastname", { required: true })} />
</FormControl>
</Flex>
<FormControl gap={6} isRequired>
<FormLabel>Company Name</FormLabel>
<Input {...form.register("company", { required: true })} />
</FormControl>
<FormControl gap={6} isRequired>
<FormLabel>Company Website</FormLabel>
<Input type="url" {...form.register("website", { required: true })} />
<FormHelperText>URL should start with https://</FormHelperText>
</FormControl>
<FormControl gap={6} isRequired>
<FormLabel>Company Social Account</FormLabel>
<Input
type="url"
{...form.register("twitterhandle", { required: true })}
/>
<FormHelperText>URL should start with https://</FormHelperText>
</FormControl>
<FormControl gap={6} isRequired>
<FormLabel>Industry</FormLabel>
<ChakraSelect
isRequired
onChange={(value) => {
if (value?.value) {
form.setValue("superchain_verticals", value.value);
}
}}
options={[
"DAOs",
"Education & Community",
"Fandom & Rewards",
"Gaming & Metaverse",
"Infra & Dev Tools",
"NFTs",
"Payments & Finance (DeFi)",
"Security & Identity",
"Social",
"Other",
].map((vertical) => ({
label: vertical,
value:
vertical === "Payments & Finance (DeFi)" ? "DeFi" : vertical,
}))}
placeholder="Select industry"
/>
</FormControl>
<FormControl gap={6} isRequired>
<FormLabel>Chain</FormLabel>
<ChakraSelect
isMulti
isRequired
onChange={(values) => {
form.setValue(
"superchain_chain",
values.map(({ value }) => value).join(";"),
);
}}
options={[
"Optimism",
"Base",
"Zora",
"Mode",
"Frax",
"Cyber",
"Redstone",
"Ancient8",
"Donatuz",
"Mantle",
"Soneium",
"Lisk",
"Arena-Z",
"Superseed",
"Ink",
].map((chain) => ({
label: chain === "Optimism" ? "OP Mainnet" : chain,
value: chain,
}))}
placeholder="Select chains"
selectedOptionStyle="check"
/>
</FormControl>
<FormControl gap={6}>
<FormLabel>Tell us more about your project</FormLabel>
<Textarea
{...form.register("what_would_you_like_to_meet_about_")}
placeholder="Tell us more about your project -- the more you share, the easier you make the approval process."
/>
<FormHelperText>Minimum 150 characters recommended.</FormHelperText>
</FormControl>
</Flex>
<div className="flex flex-row">
<Button
className="w-full"
disabled={form.formState.isSubmitting}
type="submit"
>
{form.formState.isSubmitting ? "Applying..." : "Apply now"}
</Button>
</div>
</Flex>
);
};
@@ -1,219 +0,0 @@
import { ArrowRightIcon, CircleAlertIcon } from "lucide-react";
import { useState } from "react";
import type { Team } from "@/api/team";
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { type Account, accountPlan } from "@/hooks/useApi";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
import { ApplyForOpCreditsForm } from "./ApplyForOpCreditsForm";
import { PlanCard } from "./PlanCard";
export type CreditsRecord = {
plan: Team["billingPlan"];
upTo?: true;
credits: string;
features?: string[];
};
const tier2Credits: Omit<CreditsRecord, "plan"> = {
credits: "$2,500",
features: [
"10k monthly active wallets",
"User analytics",
"Custom Auth",
"Custom Branding",
],
upTo: true,
};
const tier1Credits: Omit<CreditsRecord, "plan"> = {
credits: "$250",
upTo: true,
};
export const PlanToCreditsRecord: Record<Team["billingPlan"], CreditsRecord> = {
accelerate: {
plan: "accelerate",
...tier2Credits,
},
free: {
plan: "free",
...tier1Credits,
},
growth: {
plan: "growth",
...tier1Credits,
},
growth_legacy: {
plan: "growth_legacy",
...tier2Credits,
},
pro: {
credits: "$3,000+",
features: [
"Custom rate limits for APIs & Infra",
"Enterprise grade SLAs",
"Dedicated support",
],
plan: "pro",
upTo: true,
},
scale: {
plan: "scale",
...tier2Credits,
},
starter: {
plan: "starter",
...tier1Credits,
},
};
export function ApplyForOpCredits(props: { team: Team; account: Account }) {
const { account, team } = props;
const validTeamPlan = getValidTeamPlan(team);
const hasValidPaymentMethod = validTeamPlan !== "free";
const [hasAppliedForOpGrant] = useLocalStorage(
`appliedForOpGrant-${team.id}`,
false,
);
const isStarterPlan = validTeamPlan === "starter";
const isProPlan = validTeamPlan === "pro";
const creditsRecord = PlanToCreditsRecord[validTeamPlan];
return (
<div>
<div>
{/* credits info */}
<div className="rounded-lg border bg-card">
<div className="relative flex justify-between px-6 py-4">
<h2 className="font-semibold text-lg capitalize tracking-tight">
{creditsRecord.upTo && "Up to"} {creditsRecord.credits} Gas
Credits
</h2>
<TeamPlanBadge
className="absolute top-5 right-6"
plan={creditsRecord.plan}
teamSlug={team.slug}
/>
</div>
{/* alert */}
{!hasValidPaymentMethod && (
<div className="px-6 pb-6">
<Alert className="bg-background" variant="warning">
<CircleAlertIcon className="size-5" />
<AlertTitle>Payment method required</AlertTitle>
<AlertDescription>
You need to add a payment method to be able to claim credits.{" "}
<br /> This is to prevent abuse, you will not be charged.{" "}
<UnderlineLink
href={`/team/${props.team.slug}/~/settings/billing`}
>
Upgrade plan to get started
</UnderlineLink>
.
</AlertDescription>
</Alert>
</div>
)}
<div className="flex justify-end border-t p-4">
<ApplyOpCreditsButton
account={account}
hasAppliedForOpGrant={hasAppliedForOpGrant}
hasValidPaymentMethod={hasValidPaymentMethod}
validTeamPlan={validTeamPlan}
/>
</div>
</div>
{!isProPlan && (
<div>
<p className="my-6 text-center text-muted-foreground text-sm tracking-wide">
Or upgrade and get access to more credits
</p>
<div className="flex flex-col gap-6">
{isStarterPlan && (
<PlanCard
creditsRecord={PlanToCreditsRecord[accountPlan.growth]}
teamSlug={team.slug}
/>
)}
<PlanCard
creditsRecord={PlanToCreditsRecord[accountPlan.pro]}
teamSlug={team.slug}
/>
</div>
</div>
)}
</div>
<p className="mt-6 text-center text-muted-foreground text-sm">
We are open to distributing more than the upper limit for each tier if
you make a strong case about how it will be utilized.
</p>
</div>
);
}
function ApplyOpCreditsButton(props: {
hasAppliedForOpGrant: boolean;
hasValidPaymentMethod: boolean;
validTeamPlan: Team["billingPlan"];
account: Account;
}) {
const {
hasAppliedForOpGrant,
hasValidPaymentMethod,
validTeamPlan,
account,
} = props;
const [isOpen, setIsOpen] = useState(false);
return (
<Sheet onOpenChange={setIsOpen} open={isOpen}>
<SheetTrigger asChild>
<Button
className="gap-2"
disabled={!hasValidPaymentMethod || hasAppliedForOpGrant}
size="sm"
>
{hasAppliedForOpGrant ? (
"Already applied"
) : (
<>
Apply Now
<ArrowRightIcon className="size-4" />
</>
)}
</Button>
</SheetTrigger>
<SheetContent className="!max-w-2xl overflow-auto">
<SheetHeader>
<SheetTitle>Apply for OP credits</SheetTitle>
</SheetHeader>
<div className="h-5" />
<ApplyForOpCreditsForm
account={account}
onClose={() => {
setIsOpen(false);
}}
plan={validTeamPlan}
/>
</SheetContent>
</Sheet>
);
}
@@ -1,45 +0,0 @@
import Link from "next/link";
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
import { Button } from "@/components/ui/button";
import type { CreditsRecord } from "./ApplyForOpCreditsModal";
type PlanCardProps = {
creditsRecord: CreditsRecord;
teamSlug: string;
};
export function PlanCard({ creditsRecord, teamSlug }: PlanCardProps) {
return (
<div className="relative rounded-lg border bg-card">
<div className="flex items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-foreground text-lg tracking-tight">
{creditsRecord.upTo || "Up To"} {creditsRecord.credits} Gas Credits
</h2>
<TeamPlanBadge plan={creditsRecord.plan} teamSlug={teamSlug} />
</div>
<div className="flex flex-col gap-2 px-6 py-4">
{creditsRecord.features && (
<ul className="list-disc space-y-1 pl-4 text-muted-foreground text-sm">
{creditsRecord.features.map((feature) => (
<li className="text-muted-foreground" key={feature}>
{feature}
</li>
))}
</ul>
)}
</div>
<div className="flex justify-end border-t px-6 py-4">
<Button
asChild
className="gap-2 bg-background"
size="sm"
variant="outline"
>
<Link href={`/team/${teamSlug}/~/settings/billing`}>Upgrade</Link>
</Button>
</div>
</div>
);
}
@@ -1,42 +0,0 @@
"use server";
export async function applyOpSponsorship(params: {
fields: {
name: string;
// biome-ignore lint/suspicious/noExplicitAny: FIXME
value: any;
}[];
}) {
const { fields } = params;
if (!process.env.HUBSPOT_ACCESS_TOKEN) {
return {
error: "missing HUBSPOT_ACCESS_TOKEN",
ok: false,
};
}
const response = await fetch(
"https://api.hsforms.com/submissions/v3/integration/secure/submit/23987964/2fbf6a3b-d4cc-4a23-a4f5-42674e8487b9",
{
body: JSON.stringify({ fields }),
headers: {
Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!response.ok) {
const errorMessage = await response.text();
return {
error: errorMessage,
ok: response.ok,
};
}
return {
ok: response.ok,
};
}
@@ -1,27 +0,0 @@
import { ChakraProviderSetup } from "chakra/ChakraProviderSetup";
import { redirect } from "next/navigation";
import { getTeamBySlug } from "@/api/team";
import { getValidAccount } from "../../../../../../account/settings/getAccount";
import { SettingsGasCreditsPage } from "./SettingsCreditsPage";
export default async function Page(props: {
params: Promise<{
team_slug: string;
}>;
}) {
const params = await props.params;
const account = await getValidAccount(
`/team/${params.team_slug}/~/settings/credits`,
);
const team = await getTeamBySlug(params.team_slug);
if (!team) {
redirect("/team");
}
return (
<ChakraProviderSetup>
<SettingsGasCreditsPage account={account} team={team} />
</ChakraProviderSetup>
);
}
@@ -1,10 +1,8 @@
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../../../@/api/auth-token";
import { getValidAccount } from "../../../../../account/settings/getAccount";
import { TabPathLinks } from "@/components/ui/tabs";
import { loginRedirect } from "../../../../../login/loginRedirect";
import { SettingsLayout } from "./SettingsLayout";
export default async function Layout(props: {
params: Promise<{
@@ -14,8 +12,7 @@ export default async function Layout(props: {
}) {
const params = await props.params;
const [account, team, authToken] = await Promise.all([
getValidAccount(`/team/${params.team_slug}/~/settings`),
const [team, authToken] = await Promise.all([
getTeamBySlug(params.team_slug),
getAuthToken(),
]);
@@ -28,14 +25,38 @@ export default async function Layout(props: {
redirect("/team");
}
const client = getClientThirdwebClient({
jwt: authToken,
teamId: team.id,
});
return (
<SettingsLayout account={account} client={client} team={team}>
{props.children}
</SettingsLayout>
<div className="flex grow flex-col">
<div className="border-border pt-10 pb-5">
<div className="container max-w-6xl">
<h1 className="font-semibold text-3xl tracking-tight">
Team Settings
</h1>
</div>
</div>
<TabPathLinks
links={[
{
exactMatch: true,
name: "General",
path: `/team/${params.team_slug}/~/settings`,
},
{
name: "Members",
path: `/team/${params.team_slug}/~/settings/members`,
},
{
name: "Notifications",
path: `/team/${params.team_slug}/~/settings/notifications`,
},
]}
scrollableClassName="container max-w-6xl"
/>
<div className="container max-w-6xl flex flex-col grow gap-8 py-8">
{props.children}
</div>
</div>
);
}
@@ -132,7 +132,7 @@ export function InviteSection(props: {
<Button asChild size="sm" variant="outline">
<Link
className="gap-2"
href={`/team/${props.team.slug}/~/settings/billing?showPlans=true&highlight=growth`}
href={`/team/${props.team.slug}/~/billing?showPlans=true&highlight=growth`}
>
Upgrade
</Link>
@@ -1,6 +1,6 @@
import Link from "next/link";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { Button } from "@/components/ui/button";
import { TabPathLinks } from "../../../../../../../@/components/ui/tabs";
export default async function Layout(props: {
children: React.ReactNode;
@@ -9,53 +9,54 @@ export default async function Layout(props: {
}>;
}) {
const params = await props.params;
const usagePath = `/team/${params.team_slug}/~/usage`;
return (
<div className="flex grow flex-col">
<div className="border-border border-b py-10">
<div className="container flex flex-row justify-between">
<h1 className="font-semibold text-3xl tracking-tight lg:px-2">
Usage
</h1>
<div className="flex items-center gap-2">
<Button asChild variant="outline">
<Link href={`/team/${params.team_slug}/~/settings/billing`}>
<div className="border-border pt-10 pb-4">
<div className="container max-w-7xl flex flex-col gap-4 lg:flex-row lg:justify-between">
<h1 className="font-semibold text-3xl tracking-tight">Usage</h1>
<div className="items-center gap-3 hidden lg:flex">
<Button asChild className="rounded-full bg-card" variant="outline">
<Link href={`/team/${params.team_slug}/~/billing`}>
Billing Settings
</Link>
</Button>
<Button asChild variant="outline">
<Link href={`/team/${params.team_slug}/~/settings/invoices`}>
<Button asChild className="rounded-full bg-card" variant="outline">
<Link href={`/team/${params.team_slug}/~/billing/invoices`}>
Invoice History
</Link>
</Button>
</div>
</div>
</div>
<SidebarLayout
sidebarLinks={[
<TabPathLinks
links={[
{
exactMatch: true,
href: `/team/${params.team_slug}/~/usage`,
label: "Overview",
name: "Overview",
path: usagePath,
},
{
exactMatch: true,
href: `/team/${params.team_slug}/~/usage/rpc`,
label: "RPC",
name: "RPC",
path: `${usagePath}/rpc`,
},
{
exactMatch: true,
href: `/team/${params.team_slug}/~/usage/storage`,
label: "Storage",
name: "Storage",
path: `${usagePath}/storage`,
},
{
exactMatch: true,
href: `/team/${params.team_slug}/~/usage/account-abstraction`,
label: "Account Abstraction",
name: "Account Abstraction",
path: `${usagePath}/account-abstraction`,
},
]}
>
scrollableClassName="container max-w-7xl"
/>
<div className="flex grow flex-col container max-w-7xl pt-6 pb-10">
{props.children}
</SidebarLayout>
</div>
</div>
);
}
@@ -81,148 +81,154 @@ export default async function RPCUsage(props: {
})();
return (
<div className="container mx-auto space-y-8 py-6">
<div className="flex flex-col gap-2">
<h2 className="font-bold text-3xl tracking-tight">
RPC Usage Dashboard
</h2>
<div>
<div className="flex flex-col gap-0.5 mb-4">
<h2 className="font-semibold text-2xl tracking-tight">RPC Usage</h2>
<p className="text-muted-foreground">
Monitor your RPC usage and rate limits for the last 24 hours
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">
Plan Rate Limit
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="font-bold text-2xl capitalize">
{currentRateLimit.toLocaleString()} RPS
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">
Plan Rate Limit
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="font-bold text-2xl capitalize">
{currentRateLimit.toLocaleString()} RPS
</div>
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
</div>
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
</div>
</CardContent>
</Card>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">Peak Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="font-bold text-2xl">
{Number(peakRate.peakRPS)} RPS
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">Peak Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="font-bold text-2xl">
{Number(peakRate.peakRPS)} RPS
</div>
<div className="flex items-center gap-2">
<div
className={`h-3 w-3 rounded-full ${getStatusColor(peakPercentage)}`}
/>
<span className="text-muted-foreground text-xs">
{peakPercentage > 100
? `${(peakPercentage - 100).toFixed(0)}% over limit`
: `${peakPercentage.toFixed(0)}% of limit`}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<div
className={`h-3 w-3 rounded-full ${getStatusColor(peakPercentage)}`}
/>
<span className="text-muted-foreground text-xs">
{peakPercentage > 100
? `${(peakPercentage - 100).toFixed(0)}% over limit`
: `${peakPercentage.toFixed(0)}% of limit`}
<p className="mt-1 text-muted-foreground text-xs">
<ClockIcon className="mr-1 inline h-3 w-3" />
{peakRateDate ? peakRateDate : "No Requests in last 24 hours"}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">
Total Requests
</CardTitle>
</CardHeader>
<CardContent>
<div className="font-bold text-2xl">
{totalRequests.toLocaleString()}
</div>
<div className="mt-1 flex items-center text-muted-foreground text-xs">
<CheckCircleIcon className="mr-1 h-3 w-3 text-green-500" />
<span>
{/* we count both included and overage as included */}
{totalRequests === 0
? "0.0"
: (
((Number(totalCounts.includedCount) +
Number(totalCounts.overageCount)) /
Number(totalRequests)) *
100
).toFixed(1)}
% successful requests
</span>
</div>
</div>
<p className="mt-1 text-muted-foreground text-xs">
<ClockIcon className="mr-1 inline h-3 w-3" />
{peakRateDate ? peakRateDate : "No Requests in last 24 hours"}
</p>
</CardContent>
</Card>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">
Total Requests
</CardTitle>
</CardHeader>
<CardContent>
<div className="font-bold text-2xl">
{totalRequests.toLocaleString()}
</div>
<div className="mt-1 flex items-center text-muted-foreground text-xs">
<CheckCircleIcon className="mr-1 h-3 w-3 text-green-500" />
<span>
{/* we count both included and overage as included */}
{totalRequests === 0
? "0.0"
: (
((Number(totalCounts.includedCount) +
Number(totalCounts.overageCount)) /
Number(totalRequests)) *
100
).toFixed(1)}
% successful requests
</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">
Rate Limited
</CardTitle>
</CardHeader>
<CardContent>
<div className="font-bold text-2xl">
{totalCounts.rateLimitedCount.toLocaleString()}
</div>
<div className="mt-1 flex items-center text-muted-foreground text-xs">
<XCircleIcon className="mr-1 h-3 w-3 text-red-500" />
<span>
{/* if there are no requests, we show 100% success rate */}
{totalRequests === 0
? "0.0"
: (
(Number(totalCounts.rateLimitedCount) /
Number(totalRequests)) *
100
).toFixed(1)}
% of requests
</span>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="font-medium text-sm">Rate Limited</CardTitle>
</CardHeader>
<CardContent>
<div className="font-bold text-2xl">
{totalCounts.rateLimitedCount.toLocaleString()}
</div>
<div className="mt-1 flex items-center text-muted-foreground text-xs">
<XCircleIcon className="mr-1 h-3 w-3 text-red-500" />
<span>
{/* if there are no requests, we show 100% success rate */}
{totalRequests === 0
? "0.0"
: (
(Number(totalCounts.rateLimitedCount) /
Number(totalRequests)) *
100
).toFixed(1)}
% of requests
</span>
</div>
</CardContent>
</Card>
{peakPercentage > 100 && (
<Alert variant="destructive">
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Rate Limit Exceeded</AlertTitle>
<AlertDescription>
Your peak usage of {Number(peakRate.peakRPS)} RPS has exceeded
your plan limit of {currentRateLimit} RPS. Consider upgrading your
plan to avoid rate limiting.
</AlertDescription>
</Alert>
)}
{peakPercentage > 80 && peakPercentage <= 100 && (
<Alert
className="border-yellow-500 text-yellow-700"
variant="warning"
>
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Approaching Rate Limit</AlertTitle>
<AlertDescription>
Your peak usage is at {peakPercentage.toFixed(0)}% of your plan
limit. You may experience rate limiting during high traffic
periods.
</AlertDescription>
</Alert>
)}
<RateGraph
currentRateLimit={currentRateLimit}
data={averageRate}
peakPercentage={peakPercentage}
/>
<CountGraph
currentRateLimit={currentRateLimit}
data={averageRate}
peakPercentage={peakPercentage}
/>
</div>
{peakPercentage > 100 && (
<Alert variant="destructive">
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Rate Limit Exceeded</AlertTitle>
<AlertDescription>
Your peak usage of {Number(peakRate.peakRPS)} RPS has exceeded your
plan limit of {currentRateLimit} RPS. Consider upgrading your plan
to avoid rate limiting.
</AlertDescription>
</Alert>
)}
{peakPercentage > 80 && peakPercentage <= 100 && (
<Alert className="border-yellow-500 text-yellow-700" variant="warning">
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Approaching Rate Limit</AlertTitle>
<AlertDescription>
Your peak usage is at {peakPercentage.toFixed(0)}% of your plan
limit. You may experience rate limiting during high traffic periods.
</AlertDescription>
</Alert>
)}
<RateGraph
currentRateLimit={currentRateLimit}
data={averageRate}
peakPercentage={peakPercentage}
/>
<CountGraph
currentRateLimit={currentRateLimit}
data={averageRate}
peakPercentage={peakPercentage}
/>
</div>
);
}
@@ -151,11 +151,14 @@ export const YourFilesSection = (props: { authToken: string }) => {
return (
<div>
<h2 className="font-semibold text-lg tracking-tight">
Your Pinned Files
</h2>
<div>
<h2 className="font-semibold text-2xl tracking-tight">Your files</h2>
<p className="text-muted-foreground">
These files are stored and pinned on the IPFS
</p>
</div>
<div className="h-2" />
<div className="h-4" />
<TableContainer>
<Table>
@@ -9,7 +9,7 @@ export const SmartWalletsBillingAlert = (props: { teamSlug: string }) => {
<AlertTitle>Account Abstraction on Mainnet</AlertTitle>
<AlertDescription>
To enable AA on mainnet chains,{" "}
<UnderlineLink href={`/team/${props.teamSlug}/~/settings/billing`}>
<UnderlineLink href={`/team/${props.teamSlug}/~/billing`}>
subscribe to a billing plan.
</UnderlineLink>
</AlertDescription>
@@ -10,7 +10,7 @@ import {
SettingsIcon,
WalletIcon,
} from "lucide-react";
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
import { Badge } from "@/components/ui/badge";
import { ContractIcon } from "@/icons/ContractIcon";
import { InsightIcon } from "@/icons/InsightIcon";
@@ -1,16 +1,15 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { getProject, getProjects, type Project } from "@/api/projects";
import { getTeamBySlug, getTeams } from "@/api/team";
import { CustomChatButton } from "@/components/chat/CustomChatButton";
import { Button } from "@/components/ui/button";
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
import { SidebarProvider } from "@/components/ui/sidebar";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { AnnouncementBanner } from "../../../../../../@/components/misc/AnnouncementBanner";
import { siwaExamplePrompts } from "../../../../(dashboard)/support/definitions";
import { getValidAccount } from "../../../../account/settings/getAccount";
import { TeamHeaderLoggedIn } from "../../../components/TeamHeader/team-header-logged-in.client";
import { StaffModeNotice } from "../../(team)/_components/StaffModeNotice";
import { ProjectSidebarLayout } from "./components/ProjectSidebarLayout";
import { SaveLastUsedProject } from "./components/SaveLastUsedProject";
import { getEngineInstances } from "./engine/dedicated/_utils/getEngineInstances";
@@ -62,24 +61,12 @@ export default async function ProjectLayout(props: {
project,
});
const isStaffMode = !teams.some((t) => t.slug === team.slug);
return (
<SidebarProvider>
<div className="flex h-dvh min-w-0 grow flex-col">
{!teams.some((t) => t.slug === team.slug) && (
<div className="bg-warning-text">
<div className="container flex items-center justify-between py-4">
<div className="flex flex-col gap-2">
<p className="font-bold text-white text-xl">👀 STAFF MODE 👀</p>
<p className="text-sm text-white">
You can only view this team, not take any actions.
</p>
</div>
<Button asChild variant="default">
<Link href="/team/~">Leave Staff Mode</Link>
</Button>
</div>
</div>
)}
{isStaffMode && <StaffModeNotice />}
<div className="sticky top-0 z-10 border-border border-b bg-card">
<AnnouncementBanner />
<TeamHeaderLoggedIn
@@ -19,12 +19,14 @@ import {
getWalletUsers,
isProjectActive,
} from "@/api/analytics";
import { getAuthToken } from "@/api/auth-token";
import { getProject, type Project } from "@/api/projects";
import {
type DurationId,
getLastNDaysRange,
type Range,
} from "@/components/analytics/date-range-selector";
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
import { RangeSelector } from "@/components/analytics/range-selector";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
@@ -35,8 +37,6 @@ import type {
WalletStats,
WalletUserStats,
} from "@/types/analytics";
import { getAuthToken } from "../../../../../../@/api/auth-token";
import { LoadingChartState } from "../../../../../../@/components/analytics/empty-chart-state";
import { loginRedirect } from "../../../../login/loginRedirect";
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
@@ -623,7 +623,11 @@ export function Header(props: {
{title}
</h1>
</div>
{showRangeSelector && <RangeSelector interval={interval} range={range} />}
{showRangeSelector && (
<div className="max-sm:w-full">
<RangeSelector interval={interval} range={range} />
</div>
)}
</div>
);
}
@@ -34,7 +34,14 @@ type ExecutionResult4337Serialized =
}
| {
status: "FAILED";
error: string;
error: {
stage: string;
message: string;
errorCode: string;
inner_error: object;
nonce_used: string;
account_address: string;
};
}
| {
status: "SUBMITTED";
@@ -3,7 +3,7 @@
import { format, formatDistanceToNowStrict } from "date-fns";
import { ExternalLinkIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
import { type ThirdwebClient, toEther } from "thirdweb";
import { hexToNumber, isHex, type ThirdwebClient, toEther } from "thirdweb";
import type { Project } from "@/api/projects";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
@@ -43,10 +43,14 @@ export function TransactionDetailsUI({
const status = executionResult?.status as keyof typeof statusDetails;
const errorMessage =
executionResult && "error" in executionResult
? executionResult.error
? executionResult.error.message
: executionResult && "revertData" in executionResult
? executionResult.revertData?.revertReason
: null;
const errorDetails =
executionResult && "error" in executionResult
? executionResult.error
: undefined;
const chain = chainId ? idToChain.get(Number.parseInt(chainId)) : undefined;
const explorer = chain?.explorers?.[0];
@@ -64,7 +68,7 @@ export function TransactionDetailsUI({
// Gas information
const gasUsed =
executionResult && "actualGasUsed" in executionResult
? `${executionResult.actualGasUsed}`
? `${isHex(executionResult.actualGasUsed) ? hexToNumber(executionResult.actualGasUsed) : executionResult.actualGasUsed}`
: "N/A";
const gasCost =
@@ -244,9 +248,16 @@ export function TransactionDetailsUI({
</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
{errorMessage}
</div>
{errorDetails ? (
<CodeClient
code={JSON.stringify(errorDetails, null, 2)}
lang="json"
/>
) : (
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
{errorMessage}
</div>
)}
</CardContent>
</Card>
)}
@@ -328,7 +339,9 @@ export function TransactionDetailsUI({
Block Number
</div>
<div className="text-sm md:w-2/3">
{transaction.confirmedAtBlockNumber}
{isHex(transaction.confirmedAtBlockNumber)
? hexToNumber(transaction.confirmedAtBlockNumber)
: transaction.confirmedAtBlockNumber}
</div>
</div>
)}
@@ -5,7 +5,7 @@ import {
type Range,
} from "@/components/analytics/date-range-selector";
import { RangeSelector } from "@/components/analytics/range-selector";
import type { InAppWalletStats } from "../../../../../../../../../@/types/analytics";
import type { InAppWalletStats } from "@/types/analytics";
import { InAppWalletUsersChartCardUI } from "./InAppWalletUsersChartCard";
type InAppWalletAnalyticsProps = {
@@ -29,6 +29,7 @@ function isAbiInput(
}
// Helper to recursively get canonical type for ABI input (handles tuples)
// biome-ignore lint/suspicious/noExplicitAny: FIXME
function getCanonicalType(input: any): string {
if (input.type.startsWith("tuple")) {
// Recursively build tuple type string
@@ -93,6 +94,7 @@ export const extractEventSignatures = (
const canonicalInputs = Array.isArray(event.inputs)
? event.inputs
.filter((input: unknown) => isAbiInput(input))
// biome-ignore lint/suspicious/noExplicitAny: FIXME
.map((input: any) => getCanonicalType(input))
.join(",")
: "";
@@ -175,6 +177,7 @@ export const extractFunctionSignatures = (
const canonicalInputs = Array.isArray(func.inputs)
? func.inputs
.filter((input: unknown) => isAbiInput(input))
// biome-ignore lint/suspicious/noExplicitAny: FIXME
.map((input: any) => getCanonicalType(input))
.join(",")
: "";
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug, hasToCompleteTeamOnboarding } from "@/api/team";
import { EnsureValidConnectedWalletLoginServer } from "@/components/misc/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
import { getAuthToken } from "../../../../@/api/auth-token";
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
import {
PastDueBanner,
@@ -10,13 +10,17 @@ export function AnalyticsHeader(props: {
const { title, interval, range, showRangeSelector } = props;
return (
<div className="container flex flex-col items-start gap-3 py-10 md:flex-row md:items-center">
<div className="container max-w-7xl flex flex-col items-start gap-3 py-10 md:flex-row md:items-center">
<div className="flex-1">
<h1 className="font-semibold text-2xl tracking-tight md:text-3xl">
{title}
</h1>
</div>
{showRangeSelector && <RangeSelector interval={interval} range={range} />}
{showRangeSelector && (
<div className="max-sm:w-full">
<RangeSelector interval={interval} range={range} />
</div>
)}
</div>
);
}
@@ -102,9 +102,7 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
setHoveredTeam={setHoveredTeam}
teamsAndProjects={teamsAndProjects}
upgradeTeamLink={
currentTeam
? `/team/${currentTeam.slug}/~/settings/billing`
: undefined
currentTeam ? `/team/${currentTeam.slug}/~/billing` : undefined
}
/>
+1 -1
View File
@@ -1,4 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"extends": "//"
}
+1 -1
View File
@@ -13,7 +13,7 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "1.2.7",
"@tanstack/react-query": "5.80.7",
"@vercel/functions": "2.1.0",
"@vercel/functions": "2.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "4.1.0",
+1 -1
View File
@@ -1,4 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"extends": "//"
}
@@ -73,7 +73,7 @@ function Example() {
<${componentName}
client={client}
chain={defineChain(${options.payOptions.buyTokenChain.id})}
amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${
amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${options.payOptions.paymentMethods && options.payOptions.paymentMethods.length > 0 ? `\n\t paymentMethods={${JSON.stringify(options.payOptions.paymentMethods)}}` : ""}${
options.payOptions.widget === "transaction"
? `\n\t transaction={claimTo({
contract: nftContract,
@@ -1,5 +1,6 @@
import type { Address, Chain } from "thirdweb";
import type { Chain } from "thirdweb/chains";
import type { ThemeOverrides } from "thirdweb/react";
import type { Address } from "thirdweb/utils";
export type BridgeComponentsPlaygroundOptions = {
theme: {
@@ -22,5 +23,7 @@ export type BridgeComponentsPlaygroundOptions = {
// transaction mode options
transactionData?: string; // Simplified for demo; could be more complex in real implementation
paymentMethods: ("crypto" | "card")[];
};
};
@@ -13,6 +13,7 @@ import { useId, useState } from "react";
import type { Address } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { CustomRadioGroup } from "@/components/ui/CustomRadioGroup";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "../../../../lib/utils";
@@ -53,6 +54,8 @@ export function LeftSection(props: {
const modalTitleIconId = useId();
const modalDescriptionId = useId();
const themeId = useId();
const cryptoPaymentId = useId();
const cardPaymentId = useId();
return (
<div className="flex flex-col gap-4">
@@ -161,6 +164,65 @@ export function LeftSection(props: {
/>
</div>
</div>
{/* Payment Methods */}
<div className="flex flex-col gap-3 pt-4">
<Label>Payment Methods</Label>
<div className="flex gap-4">
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes(
"crypto",
)}
id={cryptoPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
"crypto",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
},
}));
}}
/>
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes("card")}
id={cardPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
"card",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
},
}));
}}
/>
<Label htmlFor={cardPaymentId}>Card</Label>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -249,6 +311,65 @@ export function LeftSection(props: {
/>
</div>
</div>
{/* Payment Methods */}
<div className="flex flex-col gap-3">
<Label>Payment Methods</Label>
<div className="flex gap-4">
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes(
"crypto",
)}
id={cryptoPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
"crypto",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
},
}));
}}
/>
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes("card")}
id={cardPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
"card",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
},
}));
}}
/>
<Label htmlFor={cardPaymentId}>Card</Label>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -60,6 +60,7 @@ export function RightSection(props: {
client={THIRDWEB_CLIENT}
description={props.options.payOptions.description}
image={props.options.payOptions.image}
paymentMethods={props.options.payOptions.paymentMethods}
theme={themeObj}
title={props.options.payOptions.title}
tokenAddress={props.options.payOptions.buyTokenAddress}
@@ -81,6 +82,7 @@ export function RightSection(props: {
getDefaultImage(props.options.theme.type)
}
name={props.options.payOptions.title || "Your Product Name"}
paymentMethods={props.options.payOptions.paymentMethods}
presetOptions={[1, 2, 3]}
seller={props.options.payOptions.sellerAddress}
theme={themeObj}
@@ -95,6 +97,7 @@ export function RightSection(props: {
client={THIRDWEB_CLIENT}
description={props.options.payOptions.description}
image={props.options.payOptions.image}
paymentMethods={props.options.payOptions.paymentMethods}
theme={themeObj}
title={props.options.payOptions.title}
transaction={claimTo({
@@ -13,6 +13,7 @@ const defaultConnectOptions: BridgeComponentsPlaygroundOptions = {
buyTokenChain: arbitrum,
description: "",
image: "",
paymentMethods: ["crypto", "card"],
sellerAddress: "0x0000000000000000000000000000000000000000",
title: "",
transactionData: "",
+1 -1
View File
@@ -1,4 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"extends": "//"
}
+1 -1
View File
@@ -20,7 +20,7 @@
"src/pages/**/*.{js,jsx,ts,tsx}",
"scripts/*.ts"
],
"ignore": ["src/components/ui/**"],
"ignore": ["src/components/ui/**", "src/icons/**"],
"ignoreBinaries": ["only-allow"],
"ignoreDependencies": [
"@thirdweb-dev/chains",
+18 -18
View File
@@ -33,15 +33,15 @@ import { ThirdwebIcon } from "../icons/thirdweb";
const links = [
{
href: "/connect",
name: "Connect",
name: "Wallets",
},
{
href: "/pay",
name: "Bridge",
name: "Payments",
},
{
href: "/engine",
name: "Engine",
name: "Transactions",
},
{
href: "/contracts",
@@ -181,7 +181,7 @@ const sdkLinks = [
const supportLinks = [
{
href: "/knowledge-base",
name: "Get thirdweb support",
name: "Articles",
},
{
href: "/account",
@@ -216,6 +216,20 @@ export function Header() {
</div>
<div className="flex items-center gap-3">
<div className="hidden xl:block">
<Link
className="text-foreground"
href="https://github.com/thirdweb-dev"
target="_blank"
>
<GithubIcon className="mx-3 size-6" />
</Link>
</div>
<div className="hidden xl:block">
<ThemeSwitcher className="border-none bg-transparent" />
</div>
<div className="hidden xl:block">
<DocSearch variant="search" />
</div>
@@ -231,20 +245,6 @@ export function Header() {
</Button>
</div>
<div className="hidden xl:block">
<ThemeSwitcher className="border-none bg-transparent" />
</div>
<div className="hidden xl:block">
<Link
className="text-foreground"
href="https://github.com/thirdweb-dev"
target="_blank"
>
<GithubIcon className="mx-3 size-6" />
</Link>
</div>
<div className="flex items-center gap-1 xl:hidden">
<ThemeSwitcher className="border-none bg-transparent" />
<DocSearch variant="icon" />
+1 -1
View File
@@ -9,7 +9,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
sideBar={sidebar}
sidebarHeader={
<div className="flex-col items-center gap-1">
<p className="py-5 font-semibold text-foreground text-lg">Connect</p>
<p className="py-5 font-semibold text-foreground text-lg">Wallets</p>
<PlatformSelector selected="Overview" />
</div>
}
+6 -6
View File
@@ -34,9 +34,9 @@ export const metadata = createMetadata({
},
});
# What is Connect?
# Wallets
Connect is the complete toolkit for connecting every user to your application. It features customizable onboarding flows, self-custodial in-app wallets, account abstraction, onramps, and a performant API to interact with the blockchain.
Wallets is the complete toolkit for connecting every user to your application. It features customizable onboarding flows, self-custodial in-app wallets, account abstraction, onramps, and a performant API to interact with the blockchain.
## Live Playground
@@ -55,7 +55,7 @@ You can interact with the React SDK in the [Live Playground](https://playground.
## Quick starts
Get started with Connect in your preferred language.
Get started with Wallets in your preferred language.
<Grid>
<SDKCard
@@ -78,7 +78,7 @@ Get started with Connect in your preferred language.
/>
</Grid>
## With Connect, you can
## With Wallets, you can
- **Connect to 500+ different wallet providers** with support for every EVM network
- **Log in and authenticate your users** with customizable and secure [email, phone, passkeys and social login](https://portal.thirdweb.com/connect/in-app-wallet/overview) flows.
@@ -90,7 +90,7 @@ Get started with Connect in your preferred language.
<Callout variant="info" title="Have you generated a client ID?">
<div className="flex flex-col gap-4 items-start">
<p>
You'll need a client ID to access Connect's free blockchain APIs, storage, and more.
You'll need a client ID to access the Wallet APIs, storage, and more.
</p>
<Button asChild variant='outline'>
@@ -102,7 +102,7 @@ Get started with Connect in your preferred language.
## Supported Chains
Connect is supported on every EVM compatible chain. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist).
Wallets are supported on every EVM compatible chain. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist).
<DocImage src={SupportedChains} />
+1 -1
View File
@@ -228,7 +228,7 @@ export const sidebar: SideBar = {
name: "FAQ",
},
],
name: "Onboard Users",
name: "User Wallets",
},
{ separator: true },
// User identity

Some files were not shown because too many files have changed in this diff Show More