Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
a1a6574c22 Version Packages (#5003)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-14 10:22:26 +13:00
gregandGitHub 98f358d63a fix: throw if no token is found in local storage (#4961) 2024-10-14 09:17:58 +13:00
db5457fa87 [SDK / Wallet UI] URI Connection (#4997)
Co-authored-by: Joaquim Verges <[email protected]>
2024-10-14 09:17:00 +13:00
Joaquim VergesandGitHub df0388d83b feat: expose auth options on useConnectModal (#5004) 2024-10-14 09:12:56 +13:00
kien-ngo 9685a85274 [SDK] Expose stuff for UD extensions, improve test (#4962)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `thirdweb` SDK by exposing new functionalities related to `UnstoppableDomains`, specifically the `namehash`, `reverseNameOf`, and `exists` methods, along with related error handling and utility functions.

### Detailed summary
- Added `namehash` and `reverseNameOf` functions to `UnstoppableDomains`.
- Introduced `exists` function to check the existence of a tokenId.
- Updated `resolveAddress` to utilize `exists` for validation.
- Added error handling for non-existent domain names in tests.
- Exported new types and functions from `unstoppable-domains`.

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

<!-- end pr-codex -->
2024-10-13 08:18:44 +00:00
joaquim-verges cd2c0f3868 feat: handle zksync direct deploys in deployContract (#5002)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `thirdweb` library with new features and bug fixes, particularly for zkSync deployments and improvements to various ERC1155 and ERC721 modules.

### Detailed summary
- Added support for zkSync direct deploys in `deployContract`.
- Updated `SequentialTokenIdERC1155` to include `startTokenId` in its module.
- Modified event signatures for `BatchMetadataUpdate` in both ERC721 and ERC1155.
- Increased test retry count and max concurrency in `vitest.config.ts`.
- Introduced new methods for batch metadata handling, including `getBatchIndex` and `getMetadataBatch`.
- Added `setBaseURI` functionality for both `BatchMetadataERC721` and `BatchMetadataERC1155`.
- Created parameter encoding functions for `updateTokenIdERC1155`.
- Enhanced error handling and configuration retrieval methods in modules.

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

<!-- end pr-codex -->
2024-10-13 06:30:11 +00:00
Joaquim VergesandGitHub 4f346e69e3 docs: update read-contract usage examples and explanations (#5001) 2024-10-12 22:13:53 +13:00
MananTank f9f536568e Remove scrollIntoView for activeTab (#5000)
Its creating issues with scrolling on iOS
2024-10-12 05:55:59 +00:00
MananTank 79f029cdae Only Show Top wallets in Connect Analytics, merge rest to Others + Remove dynamically generated colors (#4994)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the wallet analytics dashboard by improving the display of wallet statistics, adding new wallet types, and refining the chart configurations for better visualization of wallet usage data.

### Detailed summary
- Updated `globals.css` to remove redundant CSS variables.
- Added new wallet types in `storyUtils.ts`.
- Adjusted container widths in several story components to a maximum width of `1150px`.
- Enhanced `WalletConnectorsChartCard` to show top wallets and aggregate others.
- Updated chart configuration to handle "Others" category in both `WalletConnectorsChartCard` and `WalletDistributionChartCard`.
- Changed `ChartToShow` type to include `totalConnections` and `uniqueWalletsConnected`.
- Modified data handling in `WalletDistributionChartCard` to aggregate wallet stats correctly.
- Updated rendering logic to reflect changes in chart data and labels.

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

<!-- end pr-codex -->
2024-10-12 00:00:18 +05:30
gregfromstl 2914a50b84 Chore: deprecate iframe (#4991)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on deprecating two authentication strategies in the `types.ts` file related to in-app wallets. The strategies for `iframe_email_verification` and `iframe` are marked as deprecated, which indicates they should not be used in future implementations.

### Detailed summary
- Added `@deprecated` comment to the `iframe_email_verification` strategy.
- Added `@deprecated` comment to the `iframe` strategy.

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

<!-- end pr-codex -->
2024-10-11 16:46:44 +00:00
MananTank 1b559bf224 Add bottom padding to Sidebar (#4993)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on modifying the padding class in the `Sidebar.tsx` component to enhance the layout.

### Detailed summary
- Changed the class from `pt-7` to `py-7` in the `div` element, which adjusts the vertical padding (top and bottom) instead of just the top padding.

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

<!-- end pr-codex -->
2024-10-11 15:12:09 +00:00
Joaquim VergesandGitHub 982219a939 feat: add troubleshooting guide for React Native (#4990) 2024-10-11 13:36:03 +13:00
MananTank bed3561070 Update Banner copy (#4989)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the text of the `label` prop in the `AnnouncementBanner` component to provide a clearer and more concise message about the promotional offer for in-app wallets.

### Detailed summary
- Updated `label` prop in the `AnnouncementBanner` from:
  - 'Get 12 months of unlimited in-app wallets by applying the "FREEWALLETS" coupon in the billing dashboard'
  - To: 'Claim 12 months of free in-app wallets. Use code "FREEWALLETS". Redeem offer by October 31st!'

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

<!-- end pr-codex -->
2024-10-10 19:39:01 +00:00
MananTank 99adbe24ca Add Unlimited wallets Banner, Add Team & Legacy dashboard links in headers (#4971)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on removing the `DashboardTypeBanner` component and replacing it with the `UnlimitedWalletsBanner` in various files. It also introduces a new tooltip in the `SecondaryNav` and makes minor adjustments to the `BillingHeader` and `AnnouncementBanner`.

### Detailed summary
- Deleted `DashboardTypeBanner` component.
- Replaced instances of `TryTeamsUIBanner` and `TeamsUIBanner` with `UnlimitedWalletsBanner`.
- Added a tooltip in `SecondaryNav` for switching to the legacy dashboard.
- Simplified `BillingHeader` by changing the heading style.
- Updated `AnnouncementBanner` to include a new `UnlimitedWalletsBanner` function.

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

<!-- end pr-codex -->
2024-10-10 18:45:43 +00:00
MananTank d7e34f23e6 Add Date Range and Interval Selector on Connect Analytics page (#4988)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the date range selection functionality in the `PayAnalytics` and `ConnectAnalyticsDashboard` components by incorporating the `subDays` function and restructuring the date range handling.

### Detailed summary
- Added `subDays` import from `date-fns` for date calculations.
- Refactored `getLastNDaysRange` to use `subDays` for calculating past dates.
- Moved and restructured the `presets` rendering logic in `Filters` for clarity.
- Introduced `DateRangeSelector` and `IntervalSelector` components in `ConnectAnalyticsDashboard`.
- Replaced `useMemo` with `useState` for managing date ranges in `ConnectAnalyticsDashboard`.
- Simplified date range handling and interval type selection.

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

<!-- end pr-codex -->
2024-10-10 18:37:23 +00:00
arcoraven e06cc9b920 chore: New Engine Docs link, add some AA issues to troubleshooting guide (#4982)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the API documentation links throughout the application to point to the new `thirdweb-engine.apidocumentation.com` URLs, ensuring that users access the correct and up-to-date API references.

### Detailed summary
- Updated `API Reference` link in `sidebar.tsx`.
- Changed API reference links in `page.mdx` files for:
  - `See API reference` section.
  - `Update Chain Configuration` instructions.
  - `Cancel Transaction` references.
  - `Contract Subscriptions` references.
  - `Retry Transaction` and `Cancel` instructions.
  - `Reset Nonce` instructions.
- Added new content regarding `UserOps` and error handling.

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

<!-- end pr-codex -->
2024-10-10 17:39:01 +00:00
MananTank ec5336695b Fix unhandled server error on certain deprecated chain slugs (#4986)
Fixes: DASH-311
2024-10-10 16:25:03 +00:00
MananTank e7dd1864f9 Fix View all analytics link in contract overview page (#4987)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the URL structure in the `Analytics.tsx` component to ensure the correct routing format for the analytics link.

### Detailed summary
- Changed the `href` attribute of the `Link` component from `${chainSlug}/${contractAddress}/analytics` to `/${chainSlug}/${contractAddress}/analytics` to include a leading slash.

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

<!-- end pr-codex -->
2024-10-10 16:19:02 +00:00
kien-ngo ba9a7d4148 [Dashboard] Remove react-icons (2) (#4983)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating icon imports from `react-icons` to `lucide-react` across several components, enhancing consistency and potentially reducing bundle size.

### Detailed summary
- Replaced `FiMenu` with `MenuIcon` in `MobileMenu.tsx`.
- Replaced `FiSearch` with `SearchIcon` in `HackathonFooter.tsx`.
- Replaced `FiArrowRight` with `MoveRightIcon` in `TWQueryTable.tsx`.
- Replaced `BsFillLightningChargeFill` with `ZapIcon` in `HomePageCard.tsx` and `HeroSection.tsx`.
- Replaced `BsShieldCheck` with `ShieldCheckIcon` in `PublishedContract`.
- Replaced `FiArrowRight` and `FaEllipsisVertical` with `MoveRightIcon` and `EllipsisVerticalIcon` in `TWTable.tsx`.

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

<!-- end pr-codex -->
2024-10-10 14:50:37 +00:00
kien-ngo c776943d4e [Dashboard] Remove react-icons (1) (#4980)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating icon imports in various components, replacing some icons from `react-icons` with corresponding icons from `lucide-react`, and modifying the icon usage in the `HomePageCodeBlock`, `CustomContractForm`, `PublisherSocials`, `EventsFeedItem`, and `FileInput` components.

### Detailed summary
- Replaced `BsLightning` with `ZapIcon` in `HomePageCodeBlock`.
- Replaced `FiHelpCircle` with `InfoIcon` in `CustomContractForm`.
- Replaced `FiGlobe` with `GlobeIcon` in `PublisherSocials`.
- Replaced `FiCopy` with `CopyIcon` and `FiChevronDown` with `ChevronDownIcon` in `EventsFeedItem`.
- Replaced `FiUpload` with `UploadIcon` and `FiImage` with `ImageIcon` in `FileInput`.

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

<!-- end pr-codex -->
2024-10-10 14:50:33 +00:00
kien-ngo abcbcd0b64 [Dashboard] Fix HStack (#4984)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the layout of various components in the dashboard by adding `items-center` and `gap-2` classes to `div` elements with the `flex` class. This improves vertical alignment and spacing between elements.

### Detailed summary
- Added `items-center` and `gap-2` classes to multiple `div` elements in:
  - `airdrop-upload.tsx`
  - `AccountForm.tsx`
  - `snapshot-upload.tsx`
  - `index.tsx`
  - `Steps.tsx`
  - `DowngradeDialog.tsx`
  - `PlanCard.tsx`
  - `batch-table.tsx`
  - `UsagePage.tsx`
  - `Header.tsx`
  - `Plan.tsx`

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

<!-- end pr-codex -->
2024-10-10 14:39:51 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
3d201fc603 Version Packages (#4975)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-10 21:40:11 +13:00
kien-ngo f3fee565f9 [SDK] Improve test coverage for ERC20 extensions (#4963)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding unit tests for various functionalities related to ERC20 contracts, including metadata retrieval, contract validation, and batch token transfers.

### Detailed summary
- Added tests for `getCurrencyMetadata` to validate ERC20 contracts and return correct metadata.
- Implemented tests for `isERC20` to check contract validity against USDT, USDC, and an NFT contract.
- Created tests for `transferBatch` to verify token transfers to multiple recipients.

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

<!-- end pr-codex -->
2024-10-10 02:32:36 +00:00
gregandGitHub ec0f8d20bb [React Native SDK] Fix: Remove device share if recovering private key fails (#4976) 2024-10-10 15:27:59 +13:00
Joaquim VergesandGitHub ccd4cd6f59 fix: overestimate zksync gas limit for paymaster extra gas (#4979) 2024-10-10 12:35:47 +13:00
MananTank bd0dbb7033 Team Member Settings Page updates (#4978)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `TeamMembersSettingsPage` by adding an alert for unavailable features, updating the logic for invite permissions, and refining member management with improved filtering and sorting functionalities.

### Detailed summary
- Added an `Alert` component in `TeamMembersSettingsPage` to notify users about feature availability.
- Changed the condition for `inviteEnabled` in `InviteSection` to check for non-free plans.
- Updated member management logic to filter and sort members based on roles.
- Modified the `ManageMembersSection` to use filtered member lists for display and actions.
- Adjusted the `createMemberStub` function to accept a `createdHours` parameter.
- Updated storybook examples to reflect changes in team plans and permissions.

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

<!-- end pr-codex -->
2024-10-09 22:46:10 +00:00
kien-ngo f5b82308ee [Dashboard] Fix minor layer issues (#4928)
Before:
![image](https://github.com/user-attachments/assets/5429077d-24ee-4641-8025-137a67ab187d)

After:
![image](https://github.com/user-attachments/assets/72784965-1708-48a9-b614-970697ad40b9)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the layout and styling of components in the `supply-cards.tsx` and `page.tsx` files to enhance the visual structure and responsiveness of the dashboard interface.

### Detailed summary
- In `supply-cards.tsx`, changed the layout from a row (`flex-row`) to a column (`flex-col`) for better alignment.
- In `page.tsx`, modified the layout to use `flex-col` with additional styling adjustments, including:
  - Updated heading class from `font-bold text-xl` to `font-semibold text-2xl tracking-tight`.
  - Adjusted the container to use a grid layout for better responsiveness.

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

<!-- end pr-codex -->
2024-10-09 22:27:50 +00:00
joaquim-verges 7c103431c4 feat: add Soneium Minato chain definition (#4977)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding a new blockchain definition for `soneiumMinato` and updating the in-app wallet component to link with `Metamask` and `Passkey`.

### Detailed summary
- Added `soneiumMinato` chain definition in `packages/thirdweb/src/chains/chain-definitions/soneium-minato.ts`.
- Updated `packages/thirdweb/src/exports/chains.ts` to export `soneiumMinato`.
- Modified `apps/playground-web/src/components/in-app-wallet/profile-sections.tsx` to include buttons for linking `Metamask` and `Passkey`, replacing the `Coinbase Wallet` button.

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

<!-- end pr-codex -->
2024-10-09 21:54:57 +00:00
ElasticBottle 6a4b776ecc chore: don't override auth token / client id when hitting in app wall… (#4974)
…et url

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `thirdweb` dashboard by allowing account linking and improving the authorization logic in the `fetch.ts` file.

### Detailed summary
- Updated the authorization check in `fetch.ts` to exclude URLs from in-app wallets when sending the `authToken`.
- Introduced a new function `isInAppWalletUrl(url: string): boolean` to determine if a URL is from an in-app or embedded wallet.

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

<!-- end pr-codex -->
2024-10-09 18:30:50 +00:00
MananTank e29d80a9ec Spacing adjustments on account-abstraction policies form (#4973)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the layout and readability of the `SponsorshipPolicies` component in the `AccountAbstractionSettingsPage` by adjusting the spacing in flex containers and modifying text formatting for better clarity.

### Detailed summary
- Updated `div` elements to include `gap-6 lg:gap-12` for improved spacing.
- Changed text formatting to use `<br />` for line breaks instead of plain text.
- Made similar adjustments across multiple sections, including global spend limits and restrict to specific chains.

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

<!-- end pr-codex -->
2024-10-09 18:06:20 +00:00
MananTank 0c5b0bf007 Add /unlimited-wallets framer path in rewrites, format js files with biome (#4972)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating configuration settings, version numbers, and making minor adjustments in the codebase, including enhancing the `FRAMER_PATHS` and refining error messages in the UI.

### Detailed summary
- Added `editor.defaultFormatter` for `[javascript]` in `.vscode/settings.json`.
- Updated `version` from `5.61.3` to `5.61.4` in `packages/thirdweb/package.json`.
- Expanded `FRAMER_PATHS` in `apps/dashboard/next.config.js`.
- Added a new entry in `packages/thirdweb/CHANGELOG.md` for version `5.61.4`.
- Modified error message handling in `packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx`.
- Changed fetch URL string formatting in `apps/dashboard/next-sitemap.config.js`.
- Renamed `_path` to `path` in the `transform` function of `apps/dashboard/next-sitemap.config.js`.

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

<!-- end pr-codex -->
2024-10-09 17:25:28 +00:00
MananTank 9adf6e6b25 Prefill Promo code on coupon card from search param + scroll into view (#4968)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces functionality for pre-filling a coupon code in the `ApplyCouponCard` component based on URL search parameters and enhances the user experience by scrolling into view when a coupon code is present.

### Detailed summary
- Added `BadgeContainer` for pre-filled coupon code display in `ApplyCouponCard.stories.tsx`.
- Utilized `useSearchParams` to retrieve the coupon code from the URL in `CouponCard.tsx`.
- Enhanced `ApplyCouponCardUI` to accept `prefillPromoCode` and `scrollIntoView` props.
- Implemented scrolling behavior to focus on the form when a coupon code is present.
- Wrapped `ApplyCouponCard` in a `Suspense` component to manage loading states effectively.

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

<!-- end pr-codex -->
2024-10-09 17:10:31 +00:00
MananTank 9330401645 Include search params when redirecting to login page (#4967)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the login flow by preserving search parameters in the URL when redirecting to the login page and updating the `useLoggedInUser` hook to handle login state more effectively.

### Detailed summary
- Added `searchParamsString` to include query parameters in the login redirect.
- Updated `useLoggedInUser` to redirect based on `isLoggedIn` status instead of `redirectTo`.
- Introduced `buildLoginPath` function to construct login URLs.
- Removed unused `pathname` from `EnsureLoginPayload`.

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

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

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