Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ad7f78ca0b Version Packages (#6644)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-04 12:18:29 +02:00
Jonas DanielsandGitHub 0967a5d53a Fix newline at end of monad-testnet.ts (#6646) 2025-04-04 12:07:42 +02:00
KrishangandGitHub d93765ac54 [SDK] Feature: Export monadTestnet from thirdweb/chains (#6645) 2025-04-04 02:11:49 -07:00
169ae22a86 Add team capabilities to service-utils (#6641)
Co-authored-by: Phillip Ho <[email protected]>
2025-04-04 12:37:23 +08:00
YashandGitHub 04d2bc72e8 Update deploy config (#6643) 2025-04-04 16:55:41 +13:00
MananTank 1a236fd103 Dashboard: Storybook cleanup (#6642)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring Storybook stories across various components by standardizing the export format, updating layout parameters, and removing unnecessary imports. It aims to improve consistency and maintainability in the codebase.

### Detailed summary
- Changed `export const Desktop` to `export const Variants` in multiple stories.
- Removed `parameters: { layout: "centered" }` from many stories.
- Updated container classes for consistent styling.
- Removed `Toaster` components from stories.
- Refactored story decorators for better structure.
- Deleted unused imports like `mobileViewport` from several files.
- Consolidated `BadgeContainer` usage for better readability.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/team/components/Analytics/CombinedBarChartCard.stories.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/batchMetadata.stories.tsx`, `apps/dashboard/src/components/dashboard/StepsCard.stories.tsx`, `apps/dashboard/src/@/components/blocks/SettingsCard.stories.tsx`

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

<!-- end pr-codex -->
2025-04-03 22:02:08 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4691343c7d Version Packages (#6638)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 14:14:43 -07:00
Joaquim VergesandGitHub 93f225b8a3 [SDK] Fix ethers6 provider override (#6640) 2025-04-03 14:13:12 -07:00
Joaquim VergesandGitHub e9649a2b53 Update Next.js to 15.2.4 and other dependencies (#6632) 2025-04-03 12:51:19 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4d39fb6ee3 Version Packages (#6620)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 12:28:27 -07:00
MananTank 1e092a7bff Dashboard: Fix timestamp ordering in various charts, migrate them to use ThirdwebBarChart (#6636)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on updating various components to improve chart functionalities and data handling in the dashboard, including the introduction of the `ThirdwebBarChart`. It also refines date handling and enhances the user interface for better data representation.

### Detailed summary
- Updated date handling in `stubbedData` from February to December.
- Introduced `BadgeContainer` for displaying time ranges in `TotalSponsoredChartCard`.
- Enhanced `searchParams` structure in the analytics page to include `from` and `to` dates.
- Added `customHeader`, `toolTipValueFormatter`, and `emptyChartState` to `ThirdwebBarChart`.
- Replaced `BarChart` with `ThirdwebBarChart` in multiple components for consistency.
- Improved data mapping and sorting in `TotalSponsoredChartCard`.
- Created new stubs for `EcosystemWalletUsersChartCard` to simulate user data over various days.
- Refactored empty state components to improve user guidance when no data is available.

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

<!-- end pr-codex -->
2025-04-03 18:36:21 +00:00
MananTank 80ee3d6670 [TOOL-3950] Dashboard: link to /explore page for contracts card on chain page (#6637)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates a link in the `products.ts` file for the dashboard component, changing the URL for a specific product.

### Detailed summary
- Updated the `link` property for the product from `https://thirdweb.com/contracts` to `https://thirdweb.com/explore`.

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

<!-- end pr-codex -->
2025-04-03 17:05:00 +00:00
MananTank a7fd4e51a3 [TOOL-3946] Dashboard: Fix in-app wallet users chart timestamp incorrect ordering (#6635) 2025-04-03 13:09:56 +00:00
MananTank 61583cf6f3 Dashboard: Fix project settings page max-width (#6634)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the layout of the `settings` page in the project dashboard. It enhances the styling and structure of the header and the content area.

### Detailed summary
- Replaced the existing `<h1>` element with a new structure wrapped in a `<div>` with a class of `border-b py-10`.
- Changed the font size of the `<h1>` from `text-3xl` to `text-2xl` for better responsiveness.
- Added a container with a maximum width of `7xl` around the header.
- Adjusted the padding of the content area to `pt-6 pb-20`.

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

<!-- end pr-codex -->
2025-04-03 12:58:26 +00:00
iuwqyir cf02af06a4 fix insight blueprints navigation (#6633)
## [Dashboard] Feature: Update API path format and add token lookup endpoint

## Notes for the reviewer

This PR updates the API path format in `insightBlueprints.ts` to use curly braces for path parameters (e.g., changing `/v1/transactions/:contractAddress` to `/v1/transactions/{contractAddress}`). This standardizes our path format to match OpenAPI specifications.

## How to test

The changes can be verified by checking the updated paths in the playground interface to ensure they display correctly with the new format.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the URL path parameters in the `insightBlueprints` to use curly braces `{}` instead of colons `:` for better clarity and consistency in API endpoints.

### Detailed summary
- Changed path parameters from `:paramName` to `{paramName}` for various endpoints:
  - `/v1/transactions/:contractAddress` to `/v1/transactions/{contractAddress}`
  - `/v1/wallets/:wallet_address/transactions` to `/v1/wallets/{wallet_address}/transactions`
  - Updated multiple other endpoints similarly, including:
    - `/v1/events/:contractAddress` to `/v1/events/{contractAddress}`
    - `/v1/tokens/transfers/:contract_address` to `/v1/tokens/transfers/{contract_address}`
    - `/v1/nfts/:contract_address` to `/v1/nfts/{contract_address}`
    - And many more throughout the file.

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

<!-- end pr-codex -->
2025-04-03 12:15:39 +00:00
MananTank 6001f2487e [TOOL-3909] Dashboard: Add footer links to project pages & header style adjustments (#6627)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the layout and structure of various components in the application, introducing new footer sections, and improving the user interface for better organization and navigation.

### Detailed summary
- Removed the `SupportedPlatformLink.tsx` file.
- Updated `href` in `react-native/v5/page.mdx` from `/installation` to `/getting-started`.
- Enhanced layout in multiple components with added `container` classes for better styling.
- Introduced new `FooterCard` component in various pages for documentation and tutorial links.
- Replaced `AAFooterSection` with `AAFooter` in `AccountAbstractionPage.tsx`.
- Updated `InAppWalletFooterSection` to `InAppWalletsFooter` with a new structure.
- Modified headers and added spacing in several components for improved visual hierarchy.
- Introduced new `InsightFooter` component with relevant links.
- Adjusted various components to use consistent `container` class for layout consistency.

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

<!-- end pr-codex -->
2025-04-03 17:23:24 +05:30
Joaquim VergesandGitHub 3f21445369 [SDK] Emit accountChanged event when smart wallets switch chains (#6626) 2025-04-02 18:19:14 -07:00
MananTank 9e8fd89ec0 [TOOL-3942] Playground: Fix slow dev server - generate insight sidebar links with script (#6631)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new script for managing insight blueprints, updates the `getSidebarLinks` function to use pre-fetched blueprints, and modifies the `Page` component to display these blueprints. It also refines the utility functions for fetching blueprint data.

### Detailed summary
- Added `update-insight-blueprints` script in `package.json`.
- Created `insightBlueprints.ts` to store auto-generated blueprint data.
- Updated `getSidebarLinks` to use `insightBlueprints` instead of fetching blueprints dynamically.
- Modified `Page` component to render blueprints from `insightBlueprints`.
- Enhanced utility functions in `utils.ts` for fetching blueprint data.

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

<!-- end pr-codex -->
2025-04-02 23:28:15 +00:00
Joaquim VergesandGitHub 95a87530e3 [SDK] Fix updateListing date handling in marketplace direct listings (#6628) 2025-04-02 16:27:41 -07:00
0xFirekeeper 5faaf1be48 [Portal] Universal Bridge .NET (#6625)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on the removal of several payment-related pages in the `.NET SDK` documentation and the addition of a new section for the `Universal Bridge`, including examples and detailed API usage.

### Detailed summary
- Deleted pages:
  - `quickstart/page.mdx`
  - `buywithfiat/page.mdx`
  - `buywithcrypto/page.mdx`
  - `getbuyhistory/page.mdx`
  - `getbuywithfiatquote/page.mdx`
  - `getbuywithfiatstatus/page.mdx`
  - `getbuywithcryptoquote/page.mdx`
  - `getbuywithcryptostatus/page.mdx`
  - `getbuywithfiatcurrencies/page.mdx`
- Updated `sidebar.tsx` to remove payment links and add a new `Bridge` section.
- Introduced `Universal Bridge` documentation with:
  - Metadata creation
  - Core API examples for buying, selling, and transferring tokens.
  - Code snippets for `C#` usage of the `ThirdwebBridge`.

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

<!-- end pr-codex -->
2025-04-02 18:26:19 +00:00
jnsdls 4847ada819 Add /brand-kit to framer rewrites (#6624)
Added a rewrite rule for the `/brand-kit` path in the Framer rewrites configuration.
2025-04-02 17:53:14 +00:00
MananTank 08eea95f9b [TOOL-3764] Dashboard: Add Nebula FTUX (#6616)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `ClientIDSection` component to streamline the display of the Client ID across various components. It also modifies the `WaitingForIntegrationCard` to utilize this new component, improving code organization and reducing redundancy.

### Detailed summary
- Added `ClientIDSection` component to display the Client ID.
- Updated `InsightFTUX` to include `ClientIDSection` instead of inline code.
- Modified `WaitingForIntegrationCard` to accept children for rendering.
- Created `NebulaFTUX` component to integrate Nebula features and display Client ID.
- Adjusted styles in various components for consistency.

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

<!-- end pr-codex -->
2025-04-02 09:12:32 +00:00
MananTank 737155ba21 [TOOL-3765] Dashboard: Add Insight FTUX (#6614)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `Insight` page by introducing new components for better integration and user experience, specifically around project insights and blueprints. It refactors existing code and adds new features for displaying API usage examples.

### Detailed summary
- Updated `isProjectActive` function to accept a structured parameter.
- Added `InsightFTUX` component for displaying integration instructions.
- Introduced `BlueprintCard` component with feature descriptions.
- Modified `WaitingForIntegrationCard` to display the client ID without a period.
- Enhanced `page.tsx` to conditionally render `InsightFTUX` or `BlueprintCard` based on project status.

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

<!-- end pr-codex -->
2025-04-02 09:12:31 +00:00
MananTank 73ae5817f2 [TOOL-3763] Dashboard: Update Contracts page (#6611)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the user interface and functionality of the contracts management section. It introduces new components and modifies existing ones to improve user experience, particularly around contract deployment and management.

### Detailed summary
- Removed the `GetStartedWithContractsDeploy` component.
- Added `DeployViaCLIOrImportCard` component for contract deployment options.
- Updated the `DeployedContractsPage` to include the new `DeployViaCLIOrImportCard`.
- Modified the `ContractTableUI` to display a message and button when no contracts are present.
- Increased the height of the no contracts message section.
- Added a link to discover contracts with an external icon.

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

<!-- end pr-codex -->
2025-04-02 09:12:31 +00:00
Joaquim VergesandGitHub 6a0704a19b Connect to current wallet chain when connecting other wallets in PayEmbed (#6621) 2025-04-02 02:09:39 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2ef8f2a371 chore(deps): bump biomejs/setup-biome from 2.3.0 to 2.5.0 (#6598)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-02 01:50:04 -07:00
d6373b2c67 [SDK] Feature: added ru_RU locale to thirdweb package (#6520)
Co-authored-by: Denis Vasilev <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2025-04-02 01:46:15 -07:00
MananTank 7eaf86a607 [TOOL-3762] Dashboard: Update In-app wallet analytics page (#6610)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring components related to in-app wallets and account abstraction analytics. It introduces new loading states, replaces certain components with `StatCard`, and enhances the handling of asynchronous data fetching.

### Detailed summary
- Removed `loading.tsx` files for in-app wallets and account abstraction.
- Updated `InAppWalletsHeader` to no longer accept props.
- Replaced `Stat` with `StatCard` in `EcosystemWalletsSummary` and `AccountAbstractionSummary`.
- Added `isPending` state to `StatCard` for loading indication.
- Refactored `InAppWalletsSummary` and `AccountAbstractionSummary` to utilize async data fetching with `Suspense`.
- Enhanced `InAppWalletAnalytics` to manage loading states and data fetching more effectively.

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

<!-- end pr-codex -->
2025-04-02 07:10:08 +00:00
gregGitHubgraphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
5788cbdbad [SDK] Feature: Adds action field to Bridge transactions (#6613)
Signed-off-by: greg <[email protected]>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-04-02 00:08:03 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d9a39cde33 Version Packages (#6619)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 19:43:56 -07:00
Joaquim VergesandGitHub 62c8e7c0c4 [SDK] show transfer amounts with fees (#6618) 2025-04-01 19:20:15 -07:00
Joaquim VergesandGitHub f618c5308c [SDK] Handle entrypoint resolution inside createAndSignUserOp (#6615) 2025-04-01 18:17:17 -07:00
gregandGitHub 04943d730b [Dashboard] Feature: Token symbol search (#6617) 2025-04-01 15:06:52 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2606da6d87 Version Packages (#6606)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 13:02:59 -07:00
kumaryash90 9cf137660a Remove custom config for Ronin (#6612)
Create2 deployer restrictions are lifted by the chain. Enable deployments through default create2 factory.

<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing the custom configuration for the `Ronin` blockchain within the `thirdweb` package.

### Detailed summary
- Removed the `Ronin` configuration from the gas price settings in the `create-2-factory.ts` file.
- Updated the gas price for `Ronin` from `110n * 10n ** 9n` to a static value of `110000n`.

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

<!-- end pr-codex -->
2025-04-01 19:06:06 +00:00
MananTank 58ab8c2ee7 [TOOL-3766] Dashboard: Account Abstraction page updates (#6609)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the UI and functionality of various components in the dashboard by modifying styles, enhancing layout elements, and integrating new features related to account abstraction and user operation statistics.

### Detailed summary
- Removed `lg:gap-6` from grid classes in multiple components.
- Increased header sizes from `text-lg` to `text-xl` in `SponsoredTransactionsTableUI.tsx`.
- Adjusted margins and padding for improved spacing in various components.
- Added new `GasCreditAlert` component for displaying gas credit information.
- Integrated `AccountAbstractionSummary` into `AccountAbstractionAnalytics`.
- Updated `userOpStats` handling in `Page` component to use promises for better performance.
- Replaced certain text with more concise phrasing in descriptions.
- Removed unused imports and optimized component structure for clarity.

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

<!-- end pr-codex -->
2025-04-01 18:12:49 +00:00
MananTank 447a519ddc [TOOL-3768] Dashboard: Move Nebula from team to project layout (#6608)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `Nebula` components in the dashboard application by improving layout styles, adding project ID handling in analytics, and adjusting waitlist checks.

### Detailed summary
- Updated class names for better styling in `nebula-analytics-filter.tsx` and `nebula-waitlist-page.tsx`.
- Added `projectId` parameter to `fetchNebulaAnalytics`.
- Modified `NebulaWaitListPageUI` layout.
- Integrated `showNebula` prop in `ProjectSidebarLayout`.
- Adjusted project layout and waitlist checks in `layout.tsx`.
- Enhanced `NebulaAnalyticsPage` to pass `projectId`.
- Updated page handling in `page.tsx` to include project fetching and redirects.

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

<!-- end pr-codex -->
2025-04-01 11:56:32 +00:00
Joaquim VergesandGitHub a9b7633d05 [SDK] Show past transactions in transactions modal screen (#6607) 2025-04-01 03:14:28 -07:00
Joaquim VergesandGitHub 38e089a5f3 [SDK] Only onramp for required transaction amount (#6605) 2025-04-01 02:01:11 -07:00
Joaquim VergesandGitHub ef2f86cff0 Version Packages (#6589) 2025-03-31 23:05:33 -07:00
gregfromstl 8fdef82147 [Playground] Fix: Filter UB Backend API for only GETs (#6604)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the layout and functionality of the `Page` component in `apps/playground-web/src/app/connect/pay/backend/page.tsx`. It improves the presentation of the Universal Bridge REST API information and adds a link to view all endpoints.

### Detailed summary
- Wrapped the header and description in a `div` for better layout.
- Added a `Link` component with a button to "View all endpoints".
- Changed the title of the `BlueprintSection` from "Available endpoints" to "Available GET endpoints".
- Updated the filtering of `blueprints` to only include those with defined `get` properties.

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

<!-- end pr-codex -->
2025-04-01 01:02:26 +00:00
MananTank cbb15692a7 [TOOL-3880] Playground: Add utm_source on links (#6597)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily updates various `docsLink` URLs across multiple components in the `playground-web` application to include a query parameter (`?utm_source=playground`). This change likely aims to track traffic sources more effectively.

### Detailed summary
- Updated `docsLink` in `apps/playground-web/src/app/connect/auth/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/blockchain-api/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/commerce/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/fund-wallet/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/transactions/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/embed/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/button/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/headless/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/in-app-wallet/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/insight/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/components/blocks/APIHeader.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/backend/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/nft/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/otherLinks.ts`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/chain/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/token/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/social/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/wallet/page.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx`
- Updated `href` in `apps/playground-web/src/app/insight/page.tsx`
- Updated `href` in `apps/playground-web/src/components/auth/gated-content.tsx`
- Updated `href` in `apps/playground-web/src/components/engine/minting/erc1155-mint-to.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/sign-in/components/InAppWalletFormGroup.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/sign-in/button/LeftSection.tsx`

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

<!-- end pr-codex -->
2025-03-31 19:37:09 +00:00
MananTank 1d23e5cd9c Dashboard: Fix AA page alert styles (#6596)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `SmartWalletsBillingAlert` component in the `Alerts.tsx` file. It updates the alert's UI and removes the dismissible functionality, while also changing the import paths for components used in the alert.

### Detailed summary
- Removed unused imports related to `Alert`, `AlertDescription`, `AlertIcon`, `AlertTitle`, `Flex`, and `IconButton`.
- Updated imports for `Alert`, `AlertDescription`, `AlertTitle`, `TrackedUnderlineLink`, and `CircleAlertIcon`.
- Removed the `dismissible` prop and related logic.
- Simplified the alert structure with a new layout.
- Changed the alert icon and styling.

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

<!-- end pr-codex -->
2025-03-31 19:07:57 +00:00
MananTank a17d893811 [TOOL-3873] Fix pageview tracking in dashboard, Add posthog to playground (#6595) 2025-03-31 18:38:58 +00:00
kumaryash90 0dcde077f5 [SDK] Fix bytecode caching (#6593)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the bytecode caching mechanism in the `thirdweb` package by including additional metadata in the cache key, which improves cache differentiation and management.

### Detailed summary
- Updated the `cacheKey` in `deploy-metadata.ts` to include `compilerMetadata.publisher` and `compilerMetadata.version` along with `compilerMetadata.name` and `chain.id`.
- Removed the `version` and `publisher` fields from the deployment test case in `process-ref-deployments.test.ts`.

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

<!-- end pr-codex -->
2025-03-31 18:30:28 +00:00
jnsdls 05a041fac3 Add glossary slug route to framer rewrites (#6594)
### TL;DR

Added a rewrite rule for glossary detail pages in the dashboard app.

### What changed?

Added a new path `/learn/glossary/:glossary_slug` to the framer-rewrites.js file to support individual glossary term pages.

### How to test?

1. Navigate to a glossary term detail page (e.g., `/learn/glossary/some-term`)
2. Verify that the page loads correctly and the routing works as expected

### Why make this change?

To enable proper routing for individual glossary term pages, allowing users to access specific glossary entries directly through their unique URLs. This complements the existing glossary index page that was already configured.
2025-03-31 18:00:30 +00:00
MananTank 3d140eed97 [TOOL-3874] Portal: Fix pl.js script CSP error (#6591)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the Content Security Policy in the `next.config.mjs` file to enhance security by modifying the `script-src` directive.

### Detailed summary
- Updated the `script-src` directive to include `thirdweb.com` and `thirdweb-dev.com` without a wildcard prefix.
- Retained existing sources for `self`, `unsafe-eval`, `unsafe-inline`, `wasm-unsafe-eval`, `inline-speculation-rules`, `vercel.live`, and `js.stripe.com`.

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

<!-- end pr-codex -->
2025-03-31 17:43:57 +00:00
jnsdls a5f005fff0 Remove tutorial routes from framer rewrites (#6592)
closes: CORE-802

Redirect `/learn/tutorials` to `/learn/guides`

This PR removes `/learn/tutorials` and `/learn/tutorials/:tutorial_slug` from the framer-rewrites list and adds redirects from these paths to their corresponding `/learn/guides` equivalents.
2025-03-31 17:14:04 +00:00
saminacodes fb70cab6d4 [portal] added module warning (#6590)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a warning callout to the `page.mdx` file in the `cross-chain/agglayer` directory, informing users that the Agglayer module is in beta and has deployment limitations.

### Detailed summary
- Added a warning callout indicating that the Agglayer module is in beta.
- Specified that deployments are limited to `Sepolia` and `Cardona` due to requirements for Ethereum as L1 and zkEVM as L2.

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

<!-- end pr-codex -->
2025-03-31 14:53:39 +00:00
Joaquim VergesandGitHub cd9ea69c13 [SDK] Add erc20Value for makeOffer extension function (#6588) 2025-03-31 00:24:42 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
dc892bd136 Version Packages (#6585)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-30 16:14:40 -07:00
Joaquim VergesandGitHub 3d7c009d72 [CLI] Fix secret key handling (#6584) 2025-03-30 15:49:45 -07:00
YashandGitHub 6dbed0d4fc [SDK] fix: Use default contract params with optional overrides (#6581) 2025-03-30 15:45:13 -07:00
MananTank adee1903d5 [TOOL-3814] Dashboard: Show all active coupons in team billing page (#6568)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring the coupon management system within the billing section of the dashboard. It introduces a new structure for handling coupons, including a client component for API interactions and UI components for displaying and applying coupons.

### Detailed summary
- Deleted obsolete files: `CouponCard.tsx`, `CouponDetails.stories.tsx`, `ApplyCouponCard.stories.tsx`.
- Updated global styles import in `preview.tsx`.
- Replaced `CouponSection` with `Coupons` in `index.tsx`.
- Introduced `CouponsClient` for managing coupon API interactions.
- Created `AsyncCoupons` to fetch coupon data asynchronously.
- Developed `Coupons` component to handle loading states and display.
- Added `CouponsUI` for rendering coupon information and actions.
- Implemented coupon application and deletion logic with error handling.
- Created a story for `CouponsUI` to demonstrate different states and interactions.

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

<!-- end pr-codex -->
2025-03-30 22:18:56 +00:00
gregandGitHub a1d785b8ed Fix: Increase commerce price to avoid minimum purchase errors (#6582) 2025-03-29 20:50:25 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ba92cd2cbe Version Packages (#6575)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-29 03:41:26 -07:00
Joaquim VergesandGitHub c3d0ea0059 [PayEmbed] Respect supportedTokens prop (#6580) 2025-03-29 03:35:22 -07:00
MananTank 35c5f57179 [TOOL-3839] Fix double scrollbars in project settings page (#6579) 2025-03-29 07:51:23 +00:00
gregandGitHub 2a1f4be48b Fix: Not Found Status should show Pending (#6578) 2025-03-28 20:11:43 -07:00
gregandGitHub 0121ccd702 fix: reduce cost of playground commerce example (#6577) 2025-03-28 19:19:03 -07:00
Toomas OosaluandGitHub 12bcfc596a insight webhooks docs (#6574) 2025-03-29 11:11:25 +13:00
MananTank a4ff738268 [TOOL-3833] Dashboard: Fix overflow scroll created by sr-only element (#6576) 2025-03-28 19:17:40 +00:00
YashandGitHub b49c191355 Support stylus contracts publish and deploy (#6495) 2025-03-29 07:34:37 +13:00
saminaandGitHub 06f0765665 Update page.mdx (#6572)
Signed-off-by: samina <[email protected]>
2025-03-27 19:54:50 -07:00
309 changed files with 6906 additions and 4577 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
uses: ./.github/composite-actions/install
- name: Setup Biome
uses: biomejs/setup-biome@c016c38f26f2c4a6eb3662679143614a254263fd # v2.3.0
uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
with:
version: latest
+9 -14
View File
@@ -1,5 +1,5 @@
import type { Preview } from "@storybook/react";
import "@/styles/globals.css";
import "../src/global.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MoonIcon, SunIcon } from "lucide-react";
import { ThemeProvider, useTheme } from "next-themes";
@@ -81,23 +81,18 @@ function StoryLayout(props: {
return (
<QueryClientProvider client={queryClient}>
<div className="flex min-h-dvh min-w-0 flex-col bg-background text-foreground">
<div className="flex justify-end gap-2 border-b p-4">
<div className="fixed right-0 bottom-0 z-50 flex justify-end gap-2 p-4">
<Button
onClick={() => setTheme("dark")}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
size="sm"
variant={theme === "dark" ? "default" : "outline"}
variant="outline"
className="h-auto w-auto shrink-0 rounded-full p-2"
>
<MoonIcon className="size-4" />
</Button>
<Button
onClick={() => setTheme("light")}
size="sm"
variant={theme === "light" ? "default" : "outline"}
className="h-auto w-auto shrink-0 rounded-full p-2"
>
<SunIcon className="size-4" />
{theme === "dark" ? (
<MoonIcon className="size-4" />
) : (
<SunIcon className="size-4" />
)}
</Button>
</div>
+3 -2
View File
@@ -49,12 +49,13 @@ module.exports = [
"/templates/:template_slug",
// -- learn --
"/learn",
"/learn/tutorials",
"/learn/tutorials/:tutorial_slug",
"/learn/guides",
"/learn/guides/:guide_slug",
"/learn/courses",
"/learn/glossary",
"/learn/glossary/:glossary_slug",
// -- faucets --
"/faucets",
// -- brand kit --
"/brand-kit",
];
+4 -4
View File
@@ -68,7 +68,7 @@
"ioredis": "^5.6.0",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.483.0",
"next": "15.2.3",
"next": "15.2.4",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
@@ -106,8 +106,8 @@
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.6",
"@next/bundle-analyzer": "15.2.3",
"@next/eslint-plugin-next": "15.2.3",
"@next/bundle-analyzer": "15.2.4",
"@next/eslint-plugin-next": "15.2.4",
"@playwright/test": "1.51.1",
"@storybook/addon-essentials": "8.6.4",
"@storybook/addon-interactions": "8.6.4",
@@ -135,7 +135,7 @@
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"eslint-plugin-storybook": "0.11.6",
"eslint-plugin-storybook": "0.12.0",
"knip": "5.46.0",
"next-sitemap": "^4.2.3",
"postcss": "8.5.3",
+13
View File
@@ -361,6 +361,19 @@ async function redirects() {
"/team/:team_slug/:project_slug/connect/universal-bridge/:path*",
permanent: false,
},
// all /learn/tutorials (and sub-routes) -> /learn/guides
{
source: "/learn/tutorials/:path*",
destination: "/learn/guides/:path*",
permanent: false,
},
{
source: "/learn/tutorials",
destination: "/learn/guides",
permanent: false,
},
...legacyDashboardToTeamRedirects,
];
}
+4 -3
View File
@@ -270,9 +270,10 @@ type ActiveStatus = {
ecosystemWallet: boolean;
};
export async function isProjectActive(
params: AnalyticsQueryParams,
): Promise<ActiveStatus> {
export async function isProjectActive(params: {
teamId: string;
projectId: string;
}): Promise<ActiveStatus> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/active-usage?${searchParams.toString()}`,
@@ -8,13 +8,12 @@ import { GradientAvatar } from "./GradientAvatar";
const meta = {
title: "blocks/Avatars/GradientAvatar",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -22,14 +21,14 @@ const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<p> All images below are set with size-20 className </p>
<BadgeContainer label="No Src, No id - Skeleton">
<GradientAvatar
id={undefined}
src={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -38,7 +37,7 @@ function Story() {
<GradientAvatar
id={"foo"}
src={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -47,7 +46,7 @@ function Story() {
<GradientAvatar
id={"foo"}
src={""}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -56,7 +55,7 @@ function Story() {
<GradientAvatar
id={"bar"}
src={""}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -65,7 +64,7 @@ function Story() {
<GradientAvatar
src="invalid-src"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -74,7 +73,7 @@ function Story() {
<GradientAvatar
src="https://picsum.photos/200/300"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -83,7 +82,7 @@ function Story() {
<GradientAvatar
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -123,14 +122,14 @@ function ToggleTest() {
<GradientAvatar
src={data?.src}
id={data?.id}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="invalid Src">
<GradientAvatar
className="size-6"
className="size-20"
src={data ? "invalid-src" : undefined}
id={undefined}
client={client}
@@ -14,7 +14,7 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -22,7 +22,7 @@ const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<p> All images below are set with size-6 className </p>
<BadgeContainer label="No Src - Skeleton">
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { DangerSettingCard } from "./DangerSettingCard";
const meta = {
@@ -15,17 +15,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
@@ -9,13 +9,12 @@ import { Img } from "./Img";
const meta = {
title: "blocks/Img",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { MultiNetworkSelector } from "./NetworkSelectors";
const meta = {
@@ -16,20 +16,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-8 py-10">
<Variant label="No Chains selected by default" selectedChainIds={[]} />
<Variant
label="Polygon, Ethereum selected by default"
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SettingsCard } from "./SettingsCard";
const meta = {
@@ -15,139 +15,130 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="min-h-dvh bg-background py-6 text-foreground">
<div className="container flex max-w-[1100px] flex-col gap-10 lg:p-10">
<BadgeContainer label="No Header">
<SettingsCard
bottomText="This is bottom text"
header={undefined}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="No Header">
<SettingsCard
bottomText="This is bottom text"
header={undefined}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Header">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Header">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Error">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText="This is error text"
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Error">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText="This is error text"
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="No Permission">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText="You do not have permission to edit this"
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="No Permission">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText="You do not have permission to edit this"
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Disabled">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: true,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Disabled">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: true,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Loading">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
</div>
<BadgeContainer label="Save Loading">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
</div>
);
}
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../stories/utils";
import { SidebarLayout } from "./SidebarLayout";
const meta = {
@@ -15,17 +14,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<SidebarLayout
@@ -17,6 +17,7 @@ import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
import { CustomSidebar, type SidebarLink } from "./Sidebar";
import { AppFooter } from "./app-footer";
export function SidebarLayout(props: {
sidebarLinks: SidebarLink[];
@@ -54,15 +55,17 @@ export function FullWidthSidebarLayout(props: {
footerSidebarLinks?: SidebarLink[];
children: React.ReactNode;
className?: string;
footer?: React.ReactNode;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn("flex w-full flex-1 overflow-y-hidden", props.className)}
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar collapsible="icon" side="left">
<Sidebar collapsible="icon" side="left" className="pt-2">
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
@@ -92,10 +95,10 @@ export function FullWidthSidebarLayout(props: {
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="container z-0 flex min-w-0 max-w-[1280px] grow flex-col pb-20 max-sm:w-full lg:pt-6">
<main className="z-0 flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
{props.footer}
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
@@ -153,7 +156,7 @@ function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
const activeLink = useActiveSidebarLink(props.links);
return (
<div className="mb-4 flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator orientation="vertical" className="h-4" />
{activeLink && <span className="text-sm">{activeLink.label}</span>}
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SingleNetworkSelector } from "./NetworkSelectors";
const meta = {
@@ -16,20 +16,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-8 py-10">
<Variant label="No Chain ID selected by default" chainId={undefined} />
<Variant label="Polygon selected by default" chainId={137} />
<Variant
@@ -12,17 +12,23 @@ import Link from "next/link";
type AppFooterProps = {
className?: string;
containerClassName?: string;
};
export function AppFooter(props: AppFooterProps) {
return (
<footer
className={cn(
"w-full border-border border-t bg-card py-6 md:py-8",
"w-full border-border border-t py-6 md:py-8",
props.className,
)}
>
<div className="container flex flex-col gap-4 md:gap-6">
<div
className={cn(
"container flex flex-col gap-4 md:gap-6",
props.containerClassName,
)}
>
{/* top row */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-row items-center gap-3">
@@ -32,6 +32,7 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
};
customHeader?: React.ReactNode;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -41,7 +42,9 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
chartClassName?: string;
isPending: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
hideLabel?: boolean;
emptyChartState?: React.ReactElement;
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
@@ -65,12 +68,14 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
</CardHeader>
)}
{props.customHeader && props.customHeader}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
) : (
<BarChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
@@ -88,6 +93,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
props.hideLabel !== undefined ? props.hideLabel : true
}
labelFormatter={props.toolTipLabelFormatter}
valueFormatter={props.toolTipValueFormatter}
/>
}
/>
@@ -96,25 +102,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
content={<ChartLegendContent className="pt-5" />}
/>
)}
{configKeys.map((key, idx) => (
{configKeys.map((key) => (
<Bar
key={key}
dataKey={key}
// if stacked then they should all be the same stackId
// if grouped then they should all be unique stackId (so the key works great)
stackId={variant === "stacked" ? "a" : key}
fill={`var(--color-${key})`}
// if stacked then we need to figure out the radius based on the index in the array
// if grouped then we can just use the same radius for all
radius={
variant === "stacked"
? idx === 0
? [0, 0, 4, 4]
: idx === configKeys.length - 1
? [4, 4, 0, 0]
: [0, 0, 0, 0]
: [4, 4, 4, 4]
}
fill={props.config[key]?.color}
radius={[4, 4, 4, 4]}
strokeWidth={1}
className="stroke-background"
/>
@@ -8,7 +8,7 @@ import {
} from "@/components/ui/select";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { type CodeEnvironment, CodeSegment } from "./code-segment.client";
const meta = {
@@ -24,17 +24,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
type Mode = "default" | "no-tabs" | "only-tabs";
function Story() {
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { MultiSelect } from "./multi-select";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
@@ -36,7 +29,7 @@ function createList(len: number) {
function Story() {
return (
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
<div className="container flex max-w-6xl flex-col gap-6 py-10">
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
<VariantTest
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SelectWithSearch } from "./select-with-search";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
@@ -36,7 +29,7 @@ function createList(len: number) {
function Story() {
return (
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
<div className="container flex max-w-6xl flex-col gap-6 py-10">
<VariantTest storyLabel="5 items" listLen={5} />
<VariantTest storyLabel="5000 items" listLen={5000} />
<VariantTest
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../stories/utils";
import { BadgeContainer } from "../../stories/utils";
import { PaginationButtons } from "./pagination-buttons";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
@@ -96,7 +96,10 @@ const BreadcrumbEllipsis = ({
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
className={cn(
"relative flex h-9 w-9 items-center justify-center",
className,
)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
@@ -6,9 +6,6 @@ import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/Buttons",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { CodeClient } from "./code.client";
const meta = {
@@ -11,17 +11,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const tsCode = `\
type User = {
name: string;
@@ -86,7 +79,7 @@ const bashCode = "pnpm i thirdweb";
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="ts">
<CodeClient code={tsCode} lang="ts" />
</BadgeContainer>
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { PlainTextCodeBlock } from "./plaintext-code";
const meta = {
@@ -11,17 +11,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const jsCode = `\
import { getContract } from "thirdweb";
import { sepolia } from "thirdweb/chains";
@@ -39,7 +32,7 @@ const overflowText =
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="large chunk of code">
<PlainTextCodeBlock code={jsCode} />
</BadgeContainer>
@@ -94,7 +94,10 @@ const PaginationEllipsis = ({
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
className={cn(
"relative flex h-9 w-9 items-center justify-center",
className,
)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
@@ -13,9 +13,6 @@ import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/Select",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
@@ -10,35 +10,25 @@ import {
} from "@/components/ui/table";
import type { Meta, StoryObj } from "@storybook/react";
import Link from "next/link";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { cn } from "../../lib/utils";
import { Badge } from "./badge";
const meta = {
title: "Shadcn/Table",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="flex min-w-0 flex-col gap-6 px-4 py-6 lg:mx-auto lg:max-w-[1000px]">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="Normal">
<TableDemo />
</BadgeContainer>
+6 -2
View File
@@ -20,6 +20,7 @@ export function TabLinks(props: {
className?: string;
tabContainerClassName?: string;
shadowColor?: string;
scrollableClassName?: string;
}) {
const { containerRef, lineRef, activeTabRef } =
useUnderline<HTMLAnchorElement>();
@@ -30,7 +31,7 @@ export function TabLinks(props: {
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
<ScrollShadow
scrollableClassName="pb-[8px] relative"
scrollableClassName={cn("pb-[8px] relative", props.scrollableClassName)}
shadowColor={props.shadowColor}
>
<div
@@ -159,9 +160,11 @@ function useUnderline<El extends HTMLElement>() {
const containerRect = containerRef.current.getBoundingClientRect();
const lineEl = lineRef.current;
const rect = activeTabEl.getBoundingClientRect();
const containerPaddingLeft =
containerRect.left - containerRef.current.offsetLeft;
lineEl.style.width = `${rect.width}px`;
lineEl.style.transform = `translateX(${
rect.left - containerRect.left
rect.left - containerPaddingLeft
}px)`;
setTimeout(() => {
lineEl.style.transition = "transform 0.3s, width 0.3s";
@@ -205,6 +208,7 @@ export function TabPathLinks(props: {
className?: string;
tabContainerClassName?: string;
shadowColor?: string;
scrollableClassName?: string;
}) {
const pathname = usePathname() || "";
const { links, ...restProps } = props;
@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
placeholder="Search by token address or chain ID"
placeholder="Search with anything!"
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
defaultValue={searchParams?.get("query") || ""}
onChange={(e) => handleSearch(e.target.value)}
@@ -7,6 +7,7 @@ import {
TableRow,
} from "@/components/ui/table";
import type { Address } from "thirdweb";
import { checksumAddress } from "thirdweb/utils";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "../server/routelist-card";
@@ -34,28 +35,83 @@ async function getRoutesToRender(params: SearchParams) {
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
originTextQuery?: string;
destinationTextQuery?: string;
}> = {};
if (params.type === "origin" || typeof params.type === "undefined") {
if (params.query?.startsWith("0x")) {
filters.originTokenAddress = params.query as Address;
} else if (params.query) {
} else if (Number.isInteger(Number(params.query))) {
filters.originChainId = Number(params.query);
} else if (params.query) {
filters.originTextQuery = params.query;
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
filters.destinationTokenAddress = params.query as Address;
} else if (params.query) {
} else if (Number.isInteger(Number(params.query))) {
filters.destinationChainId = Number(params.query);
} else if (params.query) {
filters.destinationTextQuery = params.query;
}
}
// Temporary, will update this after the /routes endpoint
filters.limit = 50_000;
const routes = await getRoutes(filters);
let routes = await getRoutes({ limit: 100_000 });
const totalCount = routes.length;
if (filters.originChainId) {
routes = routes.filter(
(route) => route.originToken.chainId === filters.originChainId,
);
}
if (filters.originTokenAddress) {
const originTokenAddress = filters.originTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.originToken.address) ===
checksumAddress(originTokenAddress),
);
}
if (filters.destinationChainId) {
routes = routes.filter(
(route) => route.destinationToken.chainId === filters.destinationChainId,
);
}
if (filters.destinationTokenAddress) {
const destinationTokenAddress = filters.destinationTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.destinationToken.address) ===
checksumAddress(destinationTokenAddress),
);
}
if (filters.originTextQuery) {
const originTextQuery = filters.originTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.originToken.name.toLowerCase().includes(originTextQuery) ||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
);
});
}
if (filters.destinationTextQuery) {
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.destinationToken.name
.toLowerCase()
.includes(destinationTextQuery) ||
route.destinationToken.symbol
.toLowerCase()
.includes(destinationTextQuery)
);
});
}
return {
routesToRender: routes,
totalCount,
@@ -154,13 +210,21 @@ export async function RoutesData(props: {
out of{" "}
{filteredCount !== totalCount ? (
<>
<span className="text-accent-foreground">{filteredCount}</span>{" "}
<span className="text-accent-foreground">
{filteredCount.toLocaleString()}
</span>{" "}
routes that match filters. (Total:{" "}
<span className="text-accent-foreground">{totalCount}</span>)
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>
)
</>
) : (
<>
<span className="text-accent-foreground">{totalCount}</span> routes.
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>{" "}
routes.
</>
)}
</p>
@@ -2,43 +2,18 @@ import "server-only";
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
import { getAuthToken } from "app/api/lib/getAuthToken";
import type { Address } from "thirdweb";
import type { Route } from "./types/route";
export async function getRoutes({
limit,
offset,
originChainId,
originTokenAddress,
destinationChainId,
destinationTokenAddress,
}: {
limit?: number;
offset?: number;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
} = {}) {
const url = new URL(`${BRIDGE_URL}/v1/routes`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
const token = await getAuthToken();
const routesResponse = await fetch(url, {
headers: token
@@ -3,14 +3,11 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ZERO_ADDRESS } from "thirdweb";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import {
ErrorProvider,
type TransactionError,
} from "../../../../../../../contexts/error-handler";
import type { TransactionError } from "../../../../../../../contexts/error-handler";
import { accountStub } from "../../../../../../../stories/stubs";
import {
BatchMetadataModuleUI,
@@ -21,7 +18,6 @@ const meta = {
title: "Modules/BatchMetadata",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -31,17 +27,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
@@ -80,38 +69,35 @@ function Component() {
return (
<ThirdwebProvider>
<ErrorProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
<div className="flex items-center gap-5">
<CheckboxWithLabel
value={isOwner}
onChange={setIsOwner}
id="isOwner"
label="Is Owner"
/>
</div>
<BadgeContainer label="Default">
<BatchMetadataModuleUI
twAccount={accountStub()}
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uploadMetadata={uploadMetadataStub}
uninstallButton={{
onClick: async () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={isOwner}
contractChainId={1}
/>
</BadgeContainer>
<Toaster richColors />
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
</ErrorProvider>
<div className="flex items-center gap-5">
<CheckboxWithLabel
value={isOwner}
onChange={setIsOwner}
id="isOwner"
label="Is Owner"
/>
</div>
<BadgeContainer label="Default">
<BatchMetadataModuleUI
twAccount={accountStub()}
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uploadMetadata={uploadMetadataStub}
uninstallButton={{
onClick: async () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={isOwner}
contractChainId={1}
/>
</BadgeContainer>
</div>
</ThirdwebProvider>
);
}
@@ -11,8 +11,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { subDays } from "date-fns";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "thirdweb";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { checksumAddress } from "thirdweb/utils";
@@ -29,7 +28,6 @@ const meta = {
title: "Modules/Claimable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -39,17 +37,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
const claimCondition = {
@@ -67,7 +58,7 @@ const claimCondition = {
function Component() {
const [isOwner, setIsOwner] = useState(true);
const [name, setName] = useState("ClaimableERC721");
const [name, setName] = useState("MintableERC721");
const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false);
const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] =
useState(false);
@@ -110,7 +101,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -196,8 +187,6 @@ function Component() {
isValidTokenId={true}
noClaimConditionSet={noClaimConditionSet}
/>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -11,8 +11,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -25,7 +25,6 @@ const meta = {
title: "Modules/Mintable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -35,17 +34,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -84,7 +76,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -175,8 +167,6 @@ function Component() {
contractChainId={1}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -2,33 +2,22 @@ import { Checkbox } from "@/components/ui/checkbox";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { ThirdwebProvider } from "thirdweb/react";
import { ModuleCardUI } from "./module-card";
const meta = {
title: "Modules/ModuleCard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
@@ -120,8 +109,6 @@ function Component() {
</div>
</ModuleCardUI>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -16,7 +16,6 @@ const meta = {
title: "Modules/OpenEditionMetadata",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -26,17 +25,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
async function setSharedMetadataStub(values: SetSharedMetadataFormValues) {
@@ -98,8 +90,6 @@ function Component() {
twAccount={accountStub()}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -18,7 +18,6 @@ const meta = {
title: "Modules/Royalty",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -28,17 +27,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const _testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -79,7 +71,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -168,8 +160,6 @@ function Component() {
twAccount={twAccount}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -16,7 +16,6 @@ const meta = {
title: "Modules/Transferable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -26,17 +25,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -65,7 +57,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div className="flex gap-2">
<Checkbox
id="terms1"
@@ -139,8 +131,6 @@ function Component() {
twAccount={accountStub()}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -14,7 +14,7 @@ export const products = [
id: "contracts",
icon: ContractIcon,
description: "Pre-built contracts & deployment tools",
link: "https://thirdweb.com/contracts",
link: "https://thirdweb.com/explore",
},
{
name: "Connect SDK",
@@ -13,7 +13,7 @@ export function DeployedContractsPageHeader(props: {
const [importModalOpen, setImportModalOpen] = useState(false);
return (
<div>
<div className="border-b">
<ImportModal
isOpen={importModalOpen}
onClose={() => {
@@ -23,9 +23,11 @@ export function DeployedContractsPageHeader(props: {
projectId={props.projectId}
/>
<div className="flex flex-col gap-3 pb-6 lg:flex-row lg:items-center lg:justify-between">
<div className="container flex max-w-7xl flex-col gap-3 py-10 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="font-semibold text-3xl tracking-tight">Contracts</h1>
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
Contracts
</h1>
</div>
<div className="flex gap-3 [&>*]:grow">
<Button
@@ -0,0 +1,60 @@
"use client";
import { Button } from "@/components/ui/button";
import { ArrowUpRightIcon, DownloadIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { ImportModal } from "../../../../components/contract-components/import-contract/modal";
export function DeployViaCLIOrImportCard(props: {
teamId: string;
projectId: string;
}) {
const [importModalOpen, setImportModalOpen] = useState(false);
return (
<div className="rounded-lg border bg-card p-4 lg:p-6">
<ImportModal
isOpen={importModalOpen}
onClose={() => {
setImportModalOpen(false);
}}
teamId={props.teamId}
projectId={props.projectId}
/>
<h2 className="mb-0.5 font-semibold text-lg">
Already have a smart contract?
</h2>
<p className="max-w-2xl text-muted-foreground text-sm lg:text-base">
Import an already deployed contract or deploy a contract from source
code to easily manage permissions, upload assets, and interact with
contract functions
</p>
<div className="mt-6 flex flex-col gap-3 lg:flex-row">
<Button
variant="outline"
className="gap-2 bg-background lg:px-10"
asChild
>
<Link
href="https://portal.thirdweb.com/contracts/deploy/overview"
target="_blank"
>
Deploy via CLI
<ArrowUpRightIcon className="size-4" />
</Link>
</Button>
<Button
variant="outline"
className="gap-2 bg-background"
onClick={() => setImportModalOpen(true)}
>
<DownloadIcon className="size-4" />
Import Contract
</Button>
</div>
</div>
);
}
@@ -3,7 +3,7 @@ import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { Suspense } from "react";
import { ContractTable } from "../../../../components/contract-components/tables/contract-table";
import { DeployedContractsPageHeader } from "../DeployedContractsPageHeader";
import { GetStartedWithContractsDeploy } from "./GetStartedWithContractsDeploy";
import { DeployViaCLIOrImportCard } from "./DeployViaCLIOrImportCard";
import { getSortedDeployedContracts } from "./getSortedDeployedContracts";
export function DeployedContractsPage(props: {
@@ -17,10 +17,16 @@ export function DeployedContractsPage(props: {
teamId={props.teamId}
projectId={props.projectId}
/>
<div className="flex grow flex-col">
<div className="h-6" />
<div className="container flex max-w-7xl grow flex-col">
<Suspense fallback={<Loading />}>
<DeployedContractsPageAsync {...props} />
</Suspense>
<div className="h-8" />
<DeployViaCLIOrImportCard
teamId={props.teamId}
projectId={props.projectId}
/>
</div>
</div>
);
@@ -37,15 +43,6 @@ async function DeployedContractsPageAsync(props: {
authToken: props.authToken,
});
if (deployedContracts.length === 0) {
return (
<GetStartedWithContractsDeploy
teamId={props.teamId}
projectId={props.projectId}
/>
);
}
return (
<ClientOnly ssr={<Loading />}>
<ContractTable
@@ -1,139 +0,0 @@
"use client";
import { TabButtons } from "@/components/ui/tabs";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ImportModal } from "components/contract-components/import-contract/modal";
import { StepsCard } from "components/dashboard/StepsCard";
import { useTrack } from "hooks/analytics/useTrack";
import { useMemo, useState } from "react";
export function GetStartedWithContractsDeploy(props: {
teamId: string;
projectId: string;
}) {
const steps = useMemo(
() => [
{
title: "Build, deploy or import a contract",
description:
"Choose between deploying your own contract or import an existing one.",
children: (
<DeployOptions teamId={props.teamId} projectId={props.projectId} />
),
completed: false, // because we only show this component if the user does not have any contracts
},
],
[props.teamId, props.projectId],
);
return (
<StepsCard
title="Get started with deploying contracts"
description="This guide will help you to start deploying contracts on-chain in just a few minutes."
steps={steps}
/>
);
}
type ContentItem = {
title: string;
description: string;
href?: string;
onClick?: () => void;
};
type TabId = "explore" | "import" | "build" | "deploy";
const DeployOptions = (props: {
teamId: string;
projectId: string;
}) => {
const [showImportModal, setShowImportModal] = useState(false);
const router = useDashboardRouter();
const trackEvent = useTrack();
const content: Record<TabId, ContentItem> = useMemo(
() => ({
explore: {
title: "Ready-to-deploy",
description:
"Pick from our library of ready-to-deploy contracts and deploy to any EVM chain in just 1-click.",
href: "/explore",
},
import: {
title: "Import",
description:
"Import an already deployed contract to build apps on top of contract using thirdweb tools.",
onClick: () => setShowImportModal(true),
},
build: {
title: "Build your own",
description:
"Get started with the Solidity SDK to create custom contracts specific to your use case.",
href: "/build",
},
deploy: {
title: "Deploy from source",
description:
"Deploy your contract by using our interactive CLI. (Supports Hardhat, Forge, Truffle, and more)",
href: "https://portal.thirdweb.com/cli",
},
}),
[],
);
const [activeTab, setActiveTab] = useState<TabId>("explore");
const activeTabContent = content[activeTab];
return (
<div className="mt-3">
<ImportModal
isOpen={showImportModal}
onClose={() => {
setShowImportModal(false);
}}
teamId={props.teamId}
projectId={props.projectId}
/>
<TabButtons
tabs={Object.entries(content).map(([key, value]) => ({
key,
name: value.title,
isActive: activeTab === key,
onClick: () => setActiveTab(key as TabId),
}))}
tabClassName="font-medium"
tabContainerClassName="gap-1"
/>
<button
className="mt-3 flex w-full cursor-pointer items-center gap-2 rounded-lg border border-border bg-card p-4 hover:border-active-border"
type="button"
onClick={() => {
activeTabContent.onClick?.();
if (activeTabContent.href) {
router.push(activeTabContent.href);
}
trackEvent({
category: "your_contracts",
action: "click",
label: "deploy_options",
type: activeTab,
href: null,
isExternal: false,
});
}}
>
<div>
<h4 className="text-start font-semibold text-lg">
{activeTabContent.title}
</h4>
<p className="text-start text-muted-foreground text-sm">
{activeTabContent.description}
</p>
</div>
</button>
</div>
);
};
@@ -10,7 +10,6 @@ import {
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { Toaster } from "sonner";
import { mobileViewport } from "../../../stories/utils";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
@@ -147,7 +146,6 @@ function Variants() {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
/>
<Toaster richColors />
</div>
);
}
@@ -1,6 +1,5 @@
import type { LinkedWallet } from "@/api/linked-wallets";
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import { ThirdwebProvider } from "thirdweb/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { LinkWalletUI } from "./LinkWalletUI";
@@ -91,8 +90,6 @@ function Variants() {
accountEmail="[email protected]"
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -1,22 +0,0 @@
// app/providers.tsx
"use client";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
if (typeof window !== "undefined") {
posthog.init(
process.env.NEXT_PUBLIC_POSTHOG_API_KEY ||
"phc_hKK4bo8cHZrKuAVXfXGpfNSLSJuucUnguAgt2j6dgSV",
{
api_host: "https://a.thirdweb.com",
autocapture: true,
debug: false,
capture_pageview: true,
disable_session_recording: true,
},
);
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
return <PHProvider client={posthog}>{children}</PHProvider>;
}
@@ -1,6 +1,4 @@
import { Toaster } from "@/components/ui/sonner";
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../../../stories/utils";
import { JoinTeamPageUI } from "./JoinTeamPage";
const meta = {
@@ -16,17 +14,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div>
@@ -36,7 +27,6 @@ function Story() {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
/>
<Toaster richColors />
</div>
);
}
+7 -3
View File
@@ -8,8 +8,10 @@ import NextTopLoader from "nextjs-toploader";
import { Suspense } from "react";
import { OpCreditsGrantedModalWrapperServer } from "../components/onboarding/OpCreditsGrantedModalWrapperServer";
import { PosthogIdentifierServer } from "../components/wallets/PosthogIdentifierServer";
import { PHProvider } from "../lib/posthog/Posthog";
import { PosthogHeadSetup } from "../lib/posthog/PosthogHeadSetup";
import { PostHogPageView } from "../lib/posthog/PosthogPageView";
import { EnsureValidConnectedWalletLoginServer } from "./components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
import { PostHogProvider } from "./components/root-providers";
import { AppRouterProviders } from "./providers";
const fontSans = Inter({
@@ -63,8 +65,10 @@ export default function RootLayout({
customDomain="https://pl.thirdweb.com"
selfHosted
/>
<PosthogHeadSetup />
</head>
<PostHogProvider>
<PHProvider>
<PostHogPageView />
<body
className={cn(
"bg-background font-sans antialiased",
@@ -91,7 +95,7 @@ export default function RootLayout({
showSpinner={false}
/>
</body>
</PostHogProvider>
</PHProvider>
</html>
);
}
@@ -1,4 +1,3 @@
import { Toaster } from "@/components/ui/sonner";
import type { Meta, StoryObj } from "@storybook/react";
import { teamStub } from "../../../../stories/stubs";
import { storybookLog } from "../../../../stories/utils";
@@ -59,7 +58,6 @@ function Story(props: {
return teamStub("foo", "free");
}}
/>
<Toaster richColors />
</TeamOnboardingLayout>
);
}
@@ -2,35 +2,51 @@ import type { Meta, StoryObj } from "@storybook/react";
import { subDays } from "date-fns";
import { ThirdwebProvider } from "thirdweb/react";
import { accountStub, randomLorem } from "../../../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../../../stories/utils";
import { ChatPageLayout } from "../../components/ChatPageLayout";
import { ChatHistoryPageUI } from "./ChatHistoryPage";
const meta = {
title: "Nebula/history",
component: Story,
component: Variant,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
decorators: [
(Story) => (
<ThirdwebProvider>
<Story />
</ThirdwebProvider>
),
],
} satisfies Meta<typeof Variant>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const TenChats: Story = {
args: {
length: 10,
},
};
export const Mobile: Story = {
export const ZeroChats: Story = {
args: {
length: 0,
},
};
export const OneChat: Story = {
args: {
length: 1,
},
};
export const PrefillSearch: Story = {
args: {
length: 10,
},
parameters: {
viewport: mobileViewport("iphone14"),
prefillSearch: "xxxxxxxxxxxx",
},
};
@@ -51,30 +67,6 @@ function createRandomSessions(length: number) {
return sessions;
}
function Story() {
return (
<ThirdwebProvider>
<div className="flex flex-col gap-10 py-10">
<BadgeContainer label="10 chats">
<Variant length={10} />
</BadgeContainer>
<BadgeContainer label="0 chats">
<Variant length={0} />
</BadgeContainer>
<BadgeContainer label="1 chat">
<Variant length={1} />
</BadgeContainer>
<BadgeContainer label="No search result">
<Variant length={10} prefillSearch="xxxxxxxxxxxx" />
</BadgeContainer>
</div>
</ThirdwebProvider>
);
}
function Variant(props: {
length: number;
prefillSearch?: string;
@@ -85,7 +77,6 @@ function Variant(props: {
accountAddress="0x1234567890"
authToken="xxxxxxxx"
sessions={createRandomSessions(props.length)}
className="h-[700px] border-b lg:h-[800px]"
>
<ChatHistoryPageUI
sessions={createRandomSessions(props.length)}
@@ -1,32 +1,51 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import { ThirdwebProvider } from "thirdweb/react";
import { accountStub, randomLorem } from "../../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
import type { TruncatedSessionInfo } from "../api/types";
import { ChatPageLayout } from "./ChatPageLayout";
const meta = {
title: "Nebula/ChatPageLayout",
component: Story,
component: Variant,
decorators: [
(Story) => (
<ThirdwebProvider>
<Story />
</ThirdwebProvider>
),
],
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
} satisfies Meta<typeof Variant>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
export const NoRecentChats: Story = {
args: {
sessions: [],
},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
export const ThreeChats: Story = {
args: {
sessions: generateRandomSessions(3),
},
};
export const ThirtyChats: Story = {
args: {
sessions: generateRandomSessions(30),
},
};
export const NoEmail: Story = {
args: {
sessions: generateRandomSessions(30),
noEmail: true,
},
};
@@ -39,48 +58,26 @@ function generateRandomSessions(count: number): TruncatedSessionInfo[] {
}));
}
function Story() {
return (
<ThirdwebProvider>
<div className="flex flex-col gap-10 py-10 lg:container">
<Variant label="No Recent Chats" sessions={[]} />
<Variant label="3 Chats" sessions={generateRandomSessions(3)} />
<Variant label="30 Chats" sessions={generateRandomSessions(30)} />
<Variant
label="No Email"
sessions={generateRandomSessions(30)}
noEmail
/>
</div>
</ThirdwebProvider>
);
}
function Variant(props: {
label: string;
sessions: TruncatedSessionInfo[];
noEmail?: boolean;
}) {
return (
<BadgeContainer label={props.label}>
<ChatPageLayout
accountAddress="0xC569B9FD77d132e10954cA5E6EF617414e314b11"
authToken="xxxxx"
sessions={props.sessions}
className="h-[200px] lg:h-[800px] lg:border"
account={accountStub(
props.noEmail
? {
email: undefined,
}
: undefined,
)}
>
<div className="flex h-full w-full items-center justify-center">
CHILDREN
</div>
</ChatPageLayout>
<Toaster richColors />
</BadgeContainer>
<ChatPageLayout
accountAddress="0xC569B9FD77d132e10954cA5E6EF617414e314b11"
authToken="xxxxx"
sessions={props.sessions}
account={accountStub(
props.noEmail
? {
email: undefined,
}
: undefined,
)}
>
<div className="flex h-full w-full items-center justify-center">
CHILDREN
</div>
</ChatPageLayout>
);
}
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
import { BadgeContainer } from "../../../../stories/utils";
import { ChatBar } from "./ChatBar";
const meta = {
@@ -15,20 +15,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[800px] flex-col gap-14 py-10">
<div className="container flex max-w-6xl flex-col gap-14 py-10">
<BadgeContainer label="Not Streaming">
<ChatBar
abortChatStream={() => {}}
@@ -1,9 +1,8 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub, randomLorem } from "../../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
import { BadgeContainer } from "../../../../stories/utils";
import { type ChatMessage, Chats } from "./Chats";
const meta = {
@@ -19,17 +18,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const markdownExample = `\
# Heading 1
## Heading 2
@@ -245,8 +237,6 @@ function Story() {
]}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { Toaster } from "sonner";
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
import { BadgeContainer } from "../../../../stories/utils";
import type { NebulaContext } from "../api/chat";
import ContextFiltersButton from "./ContextFilters";
@@ -18,20 +17,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 py-10 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-8 py-10">
<Variant contextFilters={undefined} label="No Filters Set" />
<Variant
@@ -65,7 +57,6 @@ function Story() {
}}
label="chains + wallet"
/>
<Toaster richColors />
</div>
);
}
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../../stories/utils";
import { EmptyStateChatPageContent } from "./EmptyStateChatPageContent";
const meta = {
@@ -15,17 +14,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Default: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[800px] grow flex-col justify-center overflow-hidden">
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
import { BadgeContainer } from "../../../../stories/utils";
import {
ExecuteTransactionCardLayout,
type TxStatus,
@@ -22,17 +22,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const client = getThirdwebClient();
const exampleTxHash =
@@ -7,7 +7,6 @@ import {
const meta = {
title: "Banners/Billing Alerts",
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -88,10 +88,10 @@ export async function TotalSponsoredChartCardUI({
const filteredData = timeSeriesData.filter((d) => d.mainnet > 0);
return (
<div className={cn("rounded-lg border p-4 lg:p-6", className)}>
<h3 className="mb-1 font-semibold text-xl tracking-tight">
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
{title || "Total Sponsored"}
</h3>
<p className="text-muted-foreground"> {description}</p>
<p className="text-muted-foreground text-sm"> {description}</p>
<BarChart
isCurrency
chartConfig={chartConfig}
@@ -1,5 +1,5 @@
import { getProjects } from "@/api/projects";
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
import { getTeams } from "@/api/team";
import { AppFooter } from "@/components/blocks/app-footer";
import { TabPathLinks } from "@/components/ui/tabs";
import { redirect } from "next/navigation";
@@ -39,9 +39,6 @@ export default async function TeamLayout(props: {
})),
);
const isOnNebulaWaitList = (await getTeamNebulaWaitList(team.slug))
?.onWaitlist;
return (
<div className="flex h-full grow flex-col">
<AnnouncementBanner />
@@ -74,14 +71,6 @@ export default async function TeamLayout(props: {
path: `/team/${params.team_slug}/~/ecosystem`,
name: "Ecosystems",
},
...(isOnNebulaWaitList
? [
{
path: `/team/${params.team_slug}/~/nebula`,
name: "Nebula",
},
]
: []),
{
path: `/team/${params.team_slug}/~/usage`,
name: "Usage",
@@ -0,0 +1,129 @@
import type { Meta, StoryObj } from "@storybook/react";
import type { EcosystemWalletStats } from "types/analytics";
import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard";
const meta = {
title: "Ecosystem/Analytics/EcosystemWalletUsersChartCard",
component: EcosystemWalletUsersChartCard,
decorators: [
(Story) => (
<div className="container max-w-7xl py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof EcosystemWalletUsersChartCard>;
export default meta;
type Story = StoryObj<typeof meta>;
const authMethods = [
"Email",
"Google",
"Apple",
"Discord",
"Twitter",
"GitHub",
"Facebook",
"Twitch",
"LinkedIn",
"TikTok",
"Coinbase",
"MetaMask",
];
function ecosystemWalletStatsStub(
length: number,
startDate = new Date(2024, 11, 1),
): EcosystemWalletStats[] {
const stats: EcosystemWalletStats[] = [];
for (let i = 0; i < length; i++) {
const date = new Date(startDate);
date.setDate(date.getDate() + i);
const formattedDate = date.toISOString().split("T")[0] || "";
// each day, we pick between 1 and 4 auth methods
const authMethodsToPick = Math.floor(Math.random() * 4) + 1;
for (let j = 0; j < authMethodsToPick; j++) {
const authMethod =
authMethods[Math.floor(Math.random() * authMethods.length)];
stats.push({
date: formattedDate,
authenticationMethod: authMethod || "MetaMask",
uniqueWalletsConnected: Math.floor(Math.random() * 1000) + 1,
});
}
}
return stats;
}
// Empty data state
export const EmptyData: Story = {
args: {
ecosystemWalletStats: [],
isPending: false,
},
};
// Loading state
export const Loading: Story = {
args: {
ecosystemWalletStats: [],
isPending: true,
},
};
// 30 days of data
export const ThirtyDaysData: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(30),
isPending: false,
},
};
// 60 days of data
export const SixtyDaysData: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(60),
isPending: false,
},
};
// 120 days of data
export const OneHundredTwentyDaysData: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(120),
isPending: false,
},
};
// Data with lots of authentication methods to test the "Others" category
export const ManyAuthMethods: Story = {
args: {
ecosystemWalletStats: (() => {
// Generate data with 15 different auth methods to test "Others" category
const basicData = ecosystemWalletStatsStub(30);
return basicData.map((item, index) => ({
...item,
authenticationMethod:
authMethods[index % authMethods.length] || "MetaMask",
}));
})(),
isPending: false,
},
};
// Zero values test
export const ZeroValues: Story = {
args: {
ecosystemWalletStats: ecosystemWalletStatsStub(30).map((stat) => ({
...stat,
uniqueWalletsConnected: 0,
})),
isPending: false,
},
};
@@ -1,26 +1,15 @@
"use client";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
EmptyChartState,
LoadingChartState,
} from "components/analytics/empty-chart-state";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import type { ChartConfig } from "@/components/ui/chart";
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
import { UnityIcon } from "components/icons/brand-icons/UnityIcon";
import { DocLink } from "components/shared/DocLink";
import { format } from "date-fns";
import { formatTickerNumber } from "lib/format-utils";
import { formatDate } from "date-fns";
import { useMemo } from "react";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import type { EcosystemWalletStats } from "types/analytics";
import { formatTickerNumber } from "../../../../../../../../../../lib/format-utils";
type ChartData = Record<string, number> & {
time: string; // human readable date
@@ -47,7 +36,7 @@ export function EcosystemWalletUsersChartCard(props: {
// if no data for current day - create new entry
if (!chartData) {
_chartDataMap.set(stat.date, {
time: format(new Date(stat.date), "MMM dd"),
time: stat.date,
[authenticationMethod || defaultLabel]: stat.uniqueWalletsConnected,
} as ChartData);
} else if (chartData) {
@@ -114,128 +103,85 @@ export function EcosystemWalletUsersChartCard(props: {
);
return (
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Unique Users
</h3>
<p className="mb-3 text-muted-foreground text-sm">
The total number of active users in your ecosystem for each period.
</p>
<ThirdwebBarChart
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
customHeader={
<div className="relative px-6 pt-6">
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Unique Users
</h3>
<p className="mb-3 text-muted-foreground text-sm">
The total number of active users in your ecosystem for each period.
</p>
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
<ExportToCSVButton
className="bg-background"
fileName="Connect Wallets"
disabled={disableActions}
getData={async () => {
// Shows the number of each type of wallet connected on all dates
const header = ["Date", ...uniqueAuthMethods];
const rows = chartData.map((data) => {
const { time, ...rest } = data;
return [
time,
...uniqueAuthMethods.map((w) => (rest[w] || 0).toString()),
];
});
return { header, rows };
}}
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
<ExportToCSVButton
className="bg-background"
fileName="Connect Wallets"
disabled={disableActions}
getData={async () => {
// Shows the number of each type of wallet connected on all dates
const header = ["Date", ...uniqueAuthMethods];
const rows = chartData.map((data) => {
const { time, ...rest } = data;
return [
time,
...uniqueAuthMethods.map((w) => (rest[w] || 0).toString()),
];
});
return { header, rows };
}}
/>
</div>
</div>
}
config={chartConfig}
data={chartData}
isPending={props.isPending}
emptyChartState={<EcosystemWalletUsersEmptyChartState />}
showLegend
hideLabel={false}
variant="stacked"
toolTipLabelFormatter={(_v, item) => {
if (Array.isArray(item)) {
const time = item[0].payload.time as number;
return formatDate(new Date(time), "MMM d, yyyy");
}
return undefined;
}}
toolTipValueFormatter={(value) => formatTickerNumber(Number(value))}
/>
);
}
function EcosystemWalletUsersEmptyChartState() {
return (
<div className="flex flex-col items-center justify-center px-4">
<span className="mb-6 text-center text-lg">
Connect users to your app with social logins
</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
link="https://portal.thirdweb.com/typescript/v5/ecosystemWallet"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
label="React Native"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/unity/v5/wallets/ecosystem-wallet"
label="Unity"
icon={UnityIcon}
/>
</div>
{/* Chart */}
<ChartContainer
config={chartConfig}
className="h-[250px] w-full md:h-[350px]"
>
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ||
uniqueAuthMethods.every((authMethod) =>
chartData.every((data) => data[authMethod] === 0),
) ? (
<EmptyChartState>
<div className="flex flex-col items-center justify-center px-4">
<span className="mb-6 text-center text-lg">
Connect users to your app with social logins
</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
link="https://portal.thirdweb.com/typescript/v5/ecosystemWallet"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
label="React Native"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/unity/v5/wallets/ecosystem-wallet"
label="Unity"
icon={UnityIcon}
/>
</div>
</div>
</EmptyChartState>
) : (
<BarChart
accessibilityLayer
data={chartData}
margin={{
top: 20,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
tickMargin={10}
axisLine={false}
/>
<YAxis
dataKey={(data) =>
Object.entries(data)
.filter(([key]) => key !== "time")
.map(([, value]) => value)
.reduce((acc, current) => Number(acc) + Number(current), 0)
}
tickLine={false}
axisLine={false}
tickFormatter={(value) => formatTickerNumber(value)}
/>
<ChartTooltip
cursor={true}
content={
<ChartTooltipContent
valueFormatter={(value) => formatTickerNumber(Number(value))}
/>
}
/>
<ChartLegend content={<ChartLegendContent />} />
{uniqueAuthMethods.map((authMethod) => {
return (
<Bar
key={authMethod}
dataKey={authMethod}
fill={chartConfig[authMethod]?.color}
radius={4}
stackId="a"
strokeWidth={1}
className="stroke-background"
/>
);
})}
</BarChart>
)}
</ChartContainer>
</div>
);
}
@@ -1,4 +1,4 @@
import { Stat } from "components/analytics/stat";
import { StatCard } from "components/analytics/stat";
import { ActivityIcon, UserIcon } from "lucide-react";
import type { EcosystemWalletStats } from "types/analytics";
@@ -28,15 +28,17 @@ export function EcosystemWalletsSummary(props: {
return (
<div className="grid grid-cols-2 gap-4 lg:gap-6">
<Stat
<StatCard
label="Total Users"
value={allTimeStats?.uniqueWalletsConnected || 0}
icon={ActivityIcon}
isPending={false}
/>
<Stat
<StatCard
label="Monthly Active Users"
value={monthlyStats?.uniqueWalletsConnected || 0}
icon={UserIcon}
isPending={false}
/>
</div>
);
@@ -1,4 +1,3 @@
import type { Range } from "components/analytics/date-range-selector";
import { redirect } from "next/navigation";
import { getTeamBySlug } from "../../../../../../../../../@/api/team";
import { getAuthToken } from "../../../../../../../../api/lib/getAuthToken";
@@ -12,7 +11,8 @@ export default async function Page(props: {
}>;
searchParams: Promise<{
interval?: "day" | "week";
range?: Range;
from?: string;
to?: string;
}>;
}) {
const [params, searchParams] = await Promise.all([
@@ -45,7 +45,15 @@ export default async function Page(props: {
ecosystemSlug={ecosystem.slug}
teamId={team.id}
interval={searchParams.interval || "week"}
range={searchParams.range}
range={
searchParams.from && searchParams.to
? {
from: new Date(searchParams.from),
to: new Date(searchParams.to),
type: "custom",
}
: undefined
}
/>
);
}
@@ -4,9 +4,6 @@ import { EngineImportCardUI } from "./EngineImportPage";
const meta: Meta<typeof EngineImportCardUI> = {
title: "Engine/general/import",
component: EngineImportCardUI,
parameters: {
layout: "centered",
},
decorators: [
(Story) => (
<div className="py-10">
@@ -1,13 +1,11 @@
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import { EngineInstancesTableUI } from "./engine-instances-table";
const meta: Meta<typeof Story> = {
title: "Engine/general/instances",
component: Story,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -16,7 +14,6 @@ const meta: Meta<typeof Story> = {
(StoryInstance) => (
<div className="container max-w-[1154px] py-10">
<StoryInstance />
<Toaster richColors />
</div>
),
],
@@ -4,16 +4,15 @@ import type {
} from "@3rdweb-sdk/react/hooks/useEngine";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { Toaster } from "sonner";
import {
createEngineAlertRuleStub,
createEngineNotificationChannelStub,
} from "stories/stubs";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { ManageEngineAlertsSectionUI } from "./ManageEngineAlerts";
const meta = {
title: "Engine/Alerts/Manage Alerts",
title: "Engine/Alerts/ManageAlerts",
component: Story,
parameters: {
nextjs: {
@@ -25,17 +24,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
const createEngineAlertMutation = useMutation({
mutationFn: async (values: CreateNotificationChannelInput) => {
@@ -63,7 +55,7 @@ function Story() {
}
return (
<div className="container flex max-w-[1154px] flex-col gap-14 py-6">
<div className="container flex max-w-6xl flex-col gap-14 py-6">
<BadgeContainer label="2 Alerts">
<ManageEngineAlertsSectionUI
isLoading={false}
@@ -125,7 +117,6 @@ function Story() {
deleteAlert={deleteAlert}
/>
</BadgeContainer>
<Toaster richColors />
</div>
);
}
@@ -1,10 +1,9 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import {
createEngineAlertRuleStub,
createEngineAlertStub,
} from "stories/stubs";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { RecentEngineAlertsSectionUI } from "./RecentEngineAlerts";
const meta = {
@@ -20,20 +19,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1154px] flex-col gap-14 py-6">
<div className="container flex max-w-6xl flex-col gap-14 py-10">
<BadgeContainer label="3 Alerts">
<RecentEngineAlertsSectionUI
isLoading={false}
@@ -68,8 +60,6 @@ function Story() {
onAlertsUpdated={() => {}}
/>
</BadgeContainer>
<Toaster richColors />
</div>
);
}
@@ -1,44 +0,0 @@
import { getTeamBySlug } from "@/api/team";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../../login/loginRedirect";
import { NebulaAnalyticsPage } from "../../../[project_slug]/nebula/components/analytics/nebula-analytics-page";
import { NebulaWaitListPage } from "../../../[project_slug]/nebula/components/nebula-waitlist-page";
export default async function Page(props: {
params: Promise<{
team_slug: string;
}>;
searchParams: Promise<{
from: string | undefined | string[];
to: string | undefined | string[];
interval: string | undefined | string[];
}>;
}) {
const [params, searchParams] = await Promise.all([
props.params,
props.searchParams,
]);
const [authToken, team] = await Promise.all([
getAuthToken(),
getTeamBySlug(params.team_slug),
]);
if (!team || !authToken) {
loginRedirect(`/team/${params.team_slug}/~/nebula`);
}
const hasNebulaAccess = team.enabledScopes.includes("nebula");
if (hasNebulaAccess) {
return (
<NebulaAnalyticsPage
teamId={team.id}
authToken={authToken}
searchParams={searchParams}
/>
);
}
return <NebulaWaitListPage team={team} />;
}
@@ -1,10 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { addDays } from "date-fns";
import { teamStub, teamSubscriptionsStub } from "stories/stubs";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../../stories/utils";
import { BadgeContainer } from "../../../../../../../../stories/utils";
import { PlanInfoCard } from "./PlanInfoCard";
const meta = {
@@ -20,17 +17,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
const team = teamStub("foo", "growth");
const zeroUsageOnDemandSubs = teamSubscriptionsStub("plan:growth");
@@ -74,7 +64,7 @@ function Story() {
});
return (
<div className="container flex max-w-[1130px] flex-col gap-12 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-12 py-10">
<BadgeContainer label="On-demand Subscriptions with 0 usage">
<PlanInfoCard
team={team}
@@ -5,9 +5,9 @@ import { BillingPortalButton } from "@/components/billing";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { differenceInDays, isAfter } from "date-fns";
import { format } from "date-fns/format";
import { InfoIcon } from "lucide-react";
import { CircleAlertIcon } from "lucide-react";
import Link from "next/link";
import { getValidTeamPlan } from "../../../../../../components/TeamHeader/getValidTeamPlan";
@@ -37,18 +37,21 @@ export function PlanInfoCard(props: {
<div className="rounded-lg border border-border bg-card">
<div className="flex flex-col gap-4 p-4 lg:flex-row lg:items-center lg:justify-between lg:p-6">
<div>
<div className="flex flex-col items-start gap-1">
<h3 className="font-semibold text-2xl capitalize tracking-tight">
{validPlan} Plan
</h3>
<div className="flex flex-col items-start gap-0.5">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-2xl capitalize tracking-tight">
{validPlan} Plan
</h3>
{trialEndsInFuture && <Badge variant="warning">Trial</Badge>}
</div>
<p className="text-muted-foreground text-sm">
Click on "Manage Billing" to view your invoices, update your
payment method, or edit your billing details.
</p>
{trialEndsInFuture && <Badge>Trial</Badge>}
</div>
{trialEndsAfterDays > 0 && (
<p className="text-muted-foreground text-sm">
<p className="mt-4 flex items-center gap-1 text-foreground text-sm">
<InfoIcon className="size-3" />
Your trial ends in {trialEndsAfterDays} days
</p>
)}
@@ -56,6 +59,7 @@ export function PlanInfoCard(props: {
<div className="flex flex-row items-center gap-2">
{/* go to invoices page */}
<Button asChild variant="outline">
<Link href={`/team/${team.slug}/~/settings/invoices`}>
View Invoices
@@ -73,20 +77,6 @@ export function PlanInfoCard(props: {
>
Manage Billing
</BillingPortalButton>
{isActualFreePlan && (
<Button asChild variant="outline">
<TrackedLinkTW
category="account"
href="/pricing"
label="pricing-plans"
target="_blank"
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground"
>
View Pricing
</TrackedLinkTW>
</Button>
)}
</div>
</div>
@@ -1,8 +1,6 @@
import { Toaster } from "@/components/ui/sonner";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { teamStub } from "../../../../../../../stories/stubs";
import { mobileViewport } from "../../../../../../../stories/utils";
import {
DeleteTeamCard,
LeaveTeamCard,
@@ -22,22 +20,15 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testTeam = teamStub("foo", "free");
function Story() {
return (
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
<div className="container max-w-6xl py-10">
<TeamGeneralSettingsPageUI
team={testTeam}
updateTeamImage={async () => {
@@ -53,7 +44,6 @@ function Story() {
}}
/>
<ComponentVariants />
<Toaster richColors />
</div>
);
}
@@ -1,11 +1,9 @@
import type { TeamAccountRole } from "@/api/team-members";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Toaster } from "@/components/ui/sonner";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { teamStub } from "../../../../../../../stories/stubs";
import { mobileViewport } from "../../../../../../../stories/utils";
import { InviteSection } from "./InviteSection";
const meta = {
@@ -21,17 +19,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const TEAM_CONFIGS = [
{ id: "free", label: "Free Team", team: teamStub("foo", "free") },
{ id: "starter", label: "Starter Team", team: teamStub("foo", "starter") },
@@ -89,7 +80,7 @@ function Story() {
showPermissionControls && hasEditPermission === "true";
return (
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
<div className="container max-w-6xl py-10">
<div className="mb-8 flex flex-col gap-6">
<div>
<h3 className="mb-3 font-medium">Team Type</h3>
@@ -166,8 +157,6 @@ function Story() {
inviteTeamMembers={INVITE_HANDLERS[inviteResult]}
/>
</div>
<Toaster richColors />
</div>
);
}
@@ -1,13 +1,9 @@
import type { TeamInvite } from "@/api/team-invites";
import type { TeamAccountRole } from "@/api/team-members";
import { Toaster } from "@/components/ui/sonner";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { teamStub } from "../../../../../../../stories/stubs";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { BadgeContainer } from "../../../../../../../stories/utils";
import { ManageInvitesSection } from "./ManageInvitesSection";
const meta = {
@@ -23,17 +19,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const freeTeam = teamStub("foo", "free");
function createInviteStub(
@@ -74,7 +63,7 @@ const deleteInviteStub = async (inviteId: string) => {
function Story() {
return (
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
<div className="container max-w-6xl py-10">
<div className="flex flex-col gap-10">
<BadgeContainer label="Has permission">
<ManageInvitesSection
@@ -96,8 +85,6 @@ function Story() {
/>
</BadgeContainer>
</div>
<Toaster richColors />
</div>
);
}
@@ -1,12 +1,8 @@
import type { TeamAccountRole, TeamMember } from "@/api/team-members";
import { Toaster } from "@/components/ui/sonner";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { teamStub } from "../../../../../../../stories/stubs";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { BadgeContainer } from "../../../../../../../stories/utils";
import { ManageMembersSection } from "./ManageMembersSection";
const meta = {
@@ -22,17 +18,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const freeTeam = teamStub("foo", "free");
function createMemberStub(
@@ -81,7 +70,7 @@ const deleteMemberStub = async (memberId: string) => {
function Story() {
return (
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
<div className="container max-w-6xl py-10">
<div className="flex flex-col gap-10">
<BadgeContainer label="Has permission">
<ManageMembersSection
@@ -113,8 +102,6 @@ function Story() {
/>
</BadgeContainer>
</div>
<Toaster richColors />
</div>
);
}
@@ -13,9 +13,6 @@ import {
const meta = {
title: "Usage/SponsoredTransactionsTable",
component: Variant,
parameters: {
layout: "centered",
},
decorators: [
(Story) => (
<div className="container max-w-[1154px] py-10">
@@ -73,7 +73,7 @@ export function SponsoredTransactionsTableUI(
<div className="overflow-hidden rounded-lg border bg-card">
{/* header */}
<div className="flex flex-col justify-between gap-3 border-b px-4 py-4 lg:flex-row lg:items-center lg:px-6">
<h2 className="font-semibold text-lg">Sponsored Transactions</h2>
<h2 className="font-semibold text-xl">Sponsored Transactions</h2>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
{/* Filters */}
<div className="flex gap-2">
@@ -23,13 +23,13 @@ export const UsageCard: React.FC<UsageCardProps> = ({
totalUsage,
}) => {
return (
<div className="relative flex min-h-[190px] flex-col rounded-lg border border-border bg-card p-4 lg:p-6">
<h3 className="mb-1 font-semibold text-xl tracking-tight">{name}</h3>
<p className="text-muted-foreground"> {description}</p>
<div className="relative flex flex-col rounded-lg border border-border bg-card p-4 lg:p-6">
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">{name}</h3>
<p className="text-muted-foreground text-sm"> {description}</p>
<div className="h-6" />
<div className="mt-auto flex flex-col gap-1.5">
<div className="flex flex-col gap-1 text-sm">
{title && <p className="text-foreground">{title}</p>}
{total !== undefined && (
@@ -0,0 +1,22 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
export function ClientIDSection(props: {
clientId: string;
}) {
return (
<div>
<h3>Client ID</h3>
<p className="mb-2 text-muted-foreground text-sm">
Identifies your application
</p>
<CopyTextButton
textToCopy={props.clientId}
className="!h-auto w-full max-w-[400px] justify-between truncate bg-background px-3 py-3 font-mono"
textToShow={props.clientId}
copyIconPosition="right"
tooltip="Copy Client ID"
/>
</div>
);
}
@@ -29,7 +29,7 @@ export function IntegrateAPIKeyCodeTabs(props: {
isActive: tab === key,
}))}
/>
<div className="h-3" />
<div className="h-2" />
{props.tabs[tab]}
</div>
);
@@ -1,5 +1,4 @@
import type { Project } from "@/api/projects";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CodeServer } from "@/components/ui/code/code.server";
@@ -19,6 +18,7 @@ import { TypeScriptIcon } from "../../../../../../components/icons/brand-icons/T
import { UnityIcon } from "../../../../../../components/icons/brand-icons/UnityIcon";
import { UnrealIcon } from "../../../../../../components/icons/brand-icons/UnrealIcon";
import { NebulaIcon } from "../../../../../nebula-app/(app)/icons/NebulaIcon";
import { ClientIDSection } from "./ClientIDSection";
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
import { SecretKeySection } from "./SecretKeySection";
@@ -59,21 +59,7 @@ function IntegrateAPIKeySection({
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex flex-col gap-6 ">
<div>
<h3>Client ID</h3>
<p className="mb-2 text-muted-foreground text-sm">
Identifies your application.
</p>
<CopyTextButton
textToCopy={clientId}
className="!h-auto w-full max-w-[350px] justify-between truncate bg-background px-3 py-3 font-mono"
textToShow={clientId}
copyIconPosition="right"
tooltip="Copy Client ID"
/>
</div>
<ClientIDSection clientId={clientId} />
{secretKeyMasked && (
<SecretKeySection
secretKeyMasked={secretKeyMasked}
@@ -1,6 +1,5 @@
"use client";
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
import { AppFooter } from "@/components/blocks/app-footer";
import {
BookTextIcon,
BoxIcon,
@@ -12,12 +11,14 @@ import { ContractIcon } from "../../../../(dashboard)/(chain)/components/server/
import { InsightIcon } from "../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
import { PayIcon } from "../../../../(dashboard)/(chain)/components/server/icons/PayIcon";
import { SmartAccountIcon } from "../../../../(dashboard)/(chain)/components/server/icons/SmartAccountIcon";
import { NebulaIcon } from "../../../../nebula-app/(app)/icons/NebulaIcon";
export function ProjectSidebarLayout(props: {
layoutPath: string;
children: React.ReactNode;
showNebula: boolean;
}) {
const { layoutPath, children } = props;
const { layoutPath, children, showNebula } = props;
const tracking = (label: string) => ({
category: "project-sidebar",
@@ -27,7 +28,6 @@ export function ProjectSidebarLayout(props: {
return (
<FullWidthSidebarLayout
footer={<AppFooter />}
contentSidebarLinks={[
{
href: layoutPath,
@@ -60,6 +60,16 @@ export function ProjectSidebarLayout(props: {
icon: ContractIcon,
tracking: tracking("contracts"),
},
...(showNebula
? [
{
href: `${layoutPath}/nebula`,
label: "Nebula",
icon: NebulaIcon,
tracking: tracking("nebula"),
},
]
: []),
{
href: `${layoutPath}/insight`,
label: "Insight",
@@ -1,31 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import type { RpcMethodStats } from "types/analytics";
import { RpcMethodBarChartCardUI } from "./RpcMethodBarChartCardUI";
const meta = {
title: "Analytics/RpcMethodBarChartCard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
parameters: {
viewport: { defaultViewport: "desktop" },
},
};
export const Mobile: Story = {
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const generateTimeSeriesData = (
days: number,
methods: string[],
@@ -74,7 +65,7 @@ const commonMethods = [
function Component() {
return (
<div className="container space-y-8 py-8">
<div className="container max-w-6xl space-y-10 py-10">
<BadgeContainer label="Lot of RPC methods - show 10 at max, combines rest in 'Other'">
<RpcMethodBarChartCardUI
rawData={generateTimeSeriesData(30, commonMethods)}
@@ -0,0 +1,75 @@
"use client";
import { Button } from "@/components/ui/button";
import { TabButtons } from "@/components/ui/tabs";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ExternalLinkIcon } from "lucide-react";
import { useState } from "react";
export function WaitingForIntegrationCard(props: {
title: string;
codeTabs: {
code: React.ReactNode;
label: string;
}[];
ctas: {
label: string;
href: string;
trackingLabel: string;
category: string;
}[];
children?: React.ReactNode;
}) {
const [selectedTab, setSelectedTab] = useState(props.codeTabs[0]?.label);
return (
<div className="rounded-lg border bg-card">
<div className="border-b px-4 py-4 lg:px-6">
<h2 className="font-semibold text-xl tracking-tight">{props.title}</h2>
</div>
<div className="px-4 py-6 lg:p-6">
{props.children}
{/* Code */}
<div>
<TabButtons
tabClassName="!text-sm"
tabs={props.codeTabs.map((tab) => ({
name: tab.label,
onClick: () => setSelectedTab(tab.label),
isActive: tab.label === selectedTab,
}))}
/>
<div className="h-2" />
{props.codeTabs.find((tab) => tab.label === selectedTab)?.code}
</div>
</div>
<div className="flex flex-col gap-3 border-t p-4 lg:flex-row lg:items-center lg:justify-between lg:p-6">
<div className="flex gap-3">
{props.ctas.map((cta) => (
<Button asChild key={cta.label} variant="outline" size="sm">
<TrackedLinkTW
href={cta.href}
key={cta.label}
target="_blank"
className="gap-2"
category={cta.category}
label={cta.trackingLabel}
>
{cta.label}
<ExternalLinkIcon className="size-4 text-muted-foreground" />
</TrackedLinkTW>
</Button>
))}
</div>
<p className="flex items-center gap-2 rounded-full border bg-background px-3.5 py-1.5 text-sm">
<span className="!pointer-events-auto relative flex size-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-primary" />
</span>
Waiting for integration
</p>
</div>
</div>
);
}
@@ -0,0 +1,89 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { LinkIcon } from "lucide-react";
type FooterSectionProps = {
title: string;
links: {
href: string;
label: string;
}[];
};
type FooterCardProps = {
left: FooterSectionProps;
center: FooterSectionProps;
right: FooterSectionProps;
trackingCategory: string;
};
export function FooterLinksSection(props: FooterCardProps) {
return (
<div className="py-6">
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3 xl:gap-0 xl:divide-x xl:divide-border">
<div className="xl:pr-6">
<FooterSection
{...props.left}
trackingCategory={props.trackingCategory}
/>
</div>
<div className="xl:px-6">
<FooterSection
{...props.center}
trackingCategory={props.trackingCategory}
/>
</div>
<div className="xl:pl-6">
<FooterSection
{...props.right}
trackingCategory={props.trackingCategory}
/>
</div>
</div>
</div>
);
}
function FooterSection(
props: FooterSectionProps & { trackingCategory: string },
) {
return (
<div>
<h3 className="mb-2.5 font-medium">{props.title}</h3>
<div className="flex flex-col gap-2.5">
{props.links.map((link) => (
<FooterLink
key={link.label}
href={link.href}
trackingCategory={props.trackingCategory}
label={link.label}
/>
))}
</div>
</div>
);
}
function FooterLink({
href,
label,
trackingCategory,
}: {
href: string;
label: string;
trackingCategory: string;
}) {
return (
<TrackedLinkTW
href={href}
category={trackingCategory}
label={label.replace(/ /g, "-").toLowerCase()}
target="_blank"
className="flex items-start gap-2 text-balance text-muted-foreground text-sm hover:text-foreground"
>
<LinkIcon className="mt-1 size-3 shrink-0 opacity-70" />
{label}
</TrackedLinkTW>
);
}
@@ -1,108 +1,76 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { FooterLinksSection } from "../../components/footer/FooterLinksSection";
type LinkInfo = { label: string; href: string; trackingLabel: string };
export function AAFooterSection(props: {
trackingCategory: string;
}) {
export function AAFooter() {
return (
<section className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<FooterCard
title="Docs"
links={docLinks}
trackingCategory={props.trackingCategory}
/>
<FooterCard
title="Guides"
links={guideLinks}
trackingCategory={props.trackingCategory}
/>
<FooterCard
title="Templates"
links={templateLinks}
trackingCategory={props.trackingCategory}
/>
</section>
<FooterLinksSection
trackingCategory="account-abstraction"
left={{
title: "Documentation",
links: [
{
href: "https://portal.thirdweb.com/connect/account-abstraction/overview",
label: "Overview",
},
{
href: "https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started",
label: "Typescript",
},
{
href: "https://portal.thirdweb.com/react/v5/account-abstraction/get-started",
label: "React",
},
{
href: "https://portal.thirdweb.com/react/v5/account-abstraction/get-started",
label: "React Native",
},
{
href: "https://portal.thirdweb.com/dotnet/wallets/providers/account-abstraction",
label: ".NET",
},
{
href: "https://portal.thirdweb.com/unity/v5/wallets/account-abstraction",
label: "Unity",
},
{
href: "https://portal.thirdweb.com/unreal-engine/blueprints/smart-wallet",
label: "Unreal Engine",
},
],
}}
right={{
title: "Tutorials",
links: [
{
label: "Web3 onboarding with in-app wallets and AA",
href: "https://www.youtube.com/watch?v=HP8wSH7gnQA",
},
{
label: "Multi-chain experience with account abstraction",
href: "https://www.youtube.com/watch?v=2wUtq-drxAk",
},
{
label: "Add AA to a web3 unity game",
href: "https://www.youtube.com/watch?v=04G4DLhqwgI",
},
],
}}
center={{
title: "Demos",
links: [
{
label: "Connect Smart Accounts",
href: "https://playground.thirdweb.com/connect/account-abstraction/connect",
},
{
label: "Sponsor Gas Fees",
href: "https://playground.thirdweb.com/connect/account-abstraction/sponsor",
},
{
label: "Native Account Abstraction",
href: "https://playground.thirdweb.com/connect/account-abstraction/native-aa",
},
],
}}
/>
);
}
function FooterCard(props: {
title: string;
links: LinkInfo[];
trackingCategory: string;
}) {
return (
<div className="rounded-lg border border-border bg-card p-4 lg:p-6">
<h3 className="mb-5 font-semibold text-lg tracking-tight lg:text-xl">
{props.title}
</h3>
<div className="flex flex-col gap-2">
{props.links.map((link) => (
<TrackedLinkTW
key={link.href}
href={link.href}
category={props.trackingCategory}
label={link.trackingLabel}
target="_blank"
className="text-muted-foreground text-sm hover:text-foreground"
>
{link.label}
</TrackedLinkTW>
))}
</div>
</div>
);
}
const docLinks: LinkInfo[] = [
{
label: "Full Docs",
href: "https://portal.thirdweb.com/wallets/smart-wallet",
trackingLabel: "full-docs",
},
{
label: "Account Abstraction with React SDK",
href: "https://portal.thirdweb.com/wallets/smart-wallet/guides/react",
trackingLabel: "smart-wallet-react",
},
{
label: "Account Abstraction with TypeScript SDK",
href: "https://portal.thirdweb.com/wallets/smart-wallet/guides/typescript",
trackingLabel: "smart-wallet-typescript",
},
];
const guideLinks: LinkInfo[] = [
{
label: "Deploy a Smart Account (ERC-4337)",
href: "https://blog.thirdweb.com/guides/how-to-use-erc4337-smart-wallets/",
trackingLabel: "deploy-smart-wallet",
},
{
label:
"How to Extend the Base Account Abstraction Contracts Using the Solidity SDK",
href: "https://blog.thirdweb.com/guides/custom-smart-wallet-contracts/",
trackingLabel: "extend-base-smart-wallet",
},
{
label: "Batch Transactions with Account Abstraction",
href: "https://blog.thirdweb.com/guides/how-to-batch-transactions-with-the-thirdweb-sdk/",
trackingLabel: "batch-txns",
},
];
const templateLinks: LinkInfo[] = [
{
label: "Node.js template",
trackingLabel: "node-template",
href: "https://github.com/thirdweb-example/smart-wallet-script",
},
{
label: "React template",
trackingLabel: "react-template",
href: "https://github.com/thirdweb-example/smart-wallet-react",
},
];
@@ -1,100 +1,149 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { TabPathLinks } from "@/components/ui/tabs";
import { TrackedUnderlineLink } from "@/components/ui/tracked-link";
import {
TrackedLinkTW,
TrackedUnderlineLink,
} from "@/components/ui/tracked-link";
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
import { CircleAlertIcon } from "lucide-react";
import { useActiveWalletChain } from "thirdweb/react";
import type { UserOpStats } from "types/analytics";
import { AccountAbstractionSummary } from "../../../../../../components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { AAFooterSection } from "./AAFooterSection";
import { isOpChainId } from "./isOpChain";
import { useLocalStorage } from "hooks/useLocalStorage";
import { ArrowRightIcon } from "lucide-react";
import { XIcon } from "lucide-react";
import { AAFooter } from "./AAFooterSection";
const TRACKING_CATEGORY = "smart-wallet";
export function AccountAbstractionLayout(props: {
projectSlug: string;
projectId: string;
teamSlug: string;
projectKey: string;
children: React.ReactNode;
hasSmartWalletsWithoutBilling: boolean;
userOpStats: UserOpStats;
}) {
const chain = useActiveWalletChain();
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
const smartWalletsLayoutSlug = `/team/${props.teamSlug}/${props.projectSlug}/connect/account-abstraction`;
return (
<div>
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:mb-2 lg:text-3xl">
Account Abstraction
</h1>
<p className="text-muted-foreground text-sm">
Easily integrate Account abstraction (ERC-4337) compliant smart accounts
into your apps.{" "}
<TrackedUnderlineLink
target="_blank"
label="docs-wallets"
category={TRACKING_CATEGORY}
href="https://portal.thirdweb.com/wallets/smart-wallet"
>
View Documentation
</TrackedUnderlineLink>
</p>
<div className="flex grow flex-col">
{/* top */}
<div className="pt-4 lg:pt-6">
{/* header */}
<div className="container flex max-w-7xl flex-col gap-4">
<div>
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
Account Abstraction
</h1>
<p className="text-muted-foreground text-sm">
Integrate ERC-4337 compliant smart accounts for gasless
sponsorships and more. <br className="lg:hidden" />
<TrackedUnderlineLink
target="_blank"
label="docs-wallets"
category={TRACKING_CATEGORY}
href="https://portal.thirdweb.com/wallets/smart-wallet"
>
Learn more about Account Abstraction
</TrackedUnderlineLink>
</p>
</div>
{props.hasSmartWalletsWithoutBilling && <SmartWalletsBillingAlert />}
<GasCreditAlert
teamSlug={props.teamSlug}
projectId={props.projectId}
/>
</div>
<div className="h-4" />
{/* Nav */}
<TabPathLinks
scrollableClassName="container max-w-7xl"
links={[
{
name: "Analytics",
path: `${smartWalletsLayoutSlug}`,
exactMatch: true,
},
{
name: "Account Factories",
path: `${smartWalletsLayoutSlug}/factories`,
},
{
name: "Sponsorship Rules",
path: `${smartWalletsLayoutSlug}/settings`,
},
]}
/>
</div>
{/* content */}
<div className="h-6" />
<div className="flex flex-col gap-6">
{props.hasSmartWalletsWithoutBilling ? (
<SmartWalletsBillingAlert />
) : (
isOpChain && (
<Alert variant="info">
<CircleAlertIcon className="size-4" />
<AlertTitle>
Using the gas credits for OP chain paymaster
</AlertTitle>
<AlertDescription>
Credits will automatically be applied to cover gas fees for any
onchain activity across thirdweb services. <br />
Eligible chains: OP Mainnet, Base, Zora, Frax, Mode.
</AlertDescription>
</Alert>
)
)}
<div className="container flex max-w-7xl grow flex-col gap-6">
<div>{props.children}</div>
</div>
<div className="h-20" />
<div>
<AccountAbstractionSummary
aggregateUserOpUsageQuery={props.userOpStats}
/>
<div className="h-12" />
<TabPathLinks
links={[
{
name: "Analytics",
path: `${smartWalletsLayoutSlug}`,
exactMatch: true,
},
{
name: "Account Factories",
path: `${smartWalletsLayoutSlug}/factories`,
},
{
name: "Sponsorship Rules",
path: `${smartWalletsLayoutSlug}/settings`,
},
]}
/>
<div className="h-6" />
{props.children}
{/* footer */}
<div className="border-border border-t">
<div className="container max-w-7xl">
<AAFooter />
</div>
</div>
</div>
);
}
function GasCreditAlert(props: {
teamSlug: string;
projectId: string;
}) {
const [isVisible, setIsVisible] = useLocalStorage(
`gas-credit-${props.projectId}`,
true,
false,
);
if (!isVisible) return null;
return (
<div className="relative rounded-lg border border-border bg-card p-4">
<Button
onClick={() => setIsVisible(false)}
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
aria-label="Close alert"
variant="ghost"
>
<XIcon className="size-5" />
</Button>
<div>
<h2 className="mb-0.5 font-semibold">OP Gas Credit Program</h2>
<p className="text-muted-foreground text-sm">
Redeem credits towards gas Sponsorship. <br className="lg:hidden" />
<TrackedUnderlineLink
target="_blank"
label="superchain-landing"
category={TRACKING_CATEGORY}
href="https://thirdweb.com/grant/superchain"
>
Learn More
</TrackedUnderlineLink>
</p>
<div className="mt-4 flex items-center gap-4">
<Button asChild variant="outline" size="sm" className="bg-background">
<TrackedLinkTW
href={`/team/${props.teamSlug}/~/settings/credits`}
target="_blank"
className="gap-2"
category={TRACKING_CATEGORY}
label="claim-credits"
>
Claim your credits <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
</div>
<div className="h-14" />
<AAFooterSection trackingCategory={TRACKING_CATEGORY} />
</div>
);
}
@@ -1,3 +0,0 @@
export function isOpChainId(chainId: number) {
return [10, 8453, 7777777, 252, 34443].includes(chainId);
}
@@ -1,4 +1,3 @@
import { getAggregateUserOpUsage } from "@/api/analytics";
import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import type { Metadata } from "next";
@@ -34,18 +33,13 @@ export default async function Page(props: {
team.billingStatus !== "validPayment" &&
team.billingStatus !== "pastDue";
const userOpStats = await getAggregateUserOpUsage({
teamId: team.id,
projectId: project.id,
});
return (
<AccountAbstractionLayout
userOpStats={userOpStats}
projectSlug={project.slug}
teamSlug={team_slug}
projectKey={project.publishableKey}
hasSmartWalletsWithoutBilling={hasSmartWalletsWithoutBilling}
projectId={project.id}
>
{props.children}
</AccountAbstractionLayout>
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,5 +1,6 @@
import { getUserOpUsage } from "@/api/analytics";
import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import {
type Range,
@@ -8,6 +9,7 @@ import {
import { AccountAbstractionAnalytics } from "components/smart-wallets/AccountAbstractionAnalytics";
import { notFound, redirect } from "next/navigation";
import type { SearchParams } from "nuqs/server";
import { AccountAbstractionSummary } from "../../../../../../components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { searchParamLoader } from "./search-params";
@@ -31,7 +33,14 @@ export default async function Page(props: {
notFound();
}
const project = await getProject(params.team_slug, params.project_slug);
const [team, project] = await Promise.all([
getTeamBySlug(params.team_slug),
getProject(params.team_slug, params.project_slug),
]);
if (!team) {
redirect("/team");
}
if (!project) {
redirect(`/team/${params.team_slug}`);
@@ -61,12 +70,20 @@ export default async function Page(props: {
});
return (
<AccountAbstractionAnalytics
userOpStats={userOpStats}
client={getThirdwebClient(authToken)}
teamId={project.teamId}
projectId={project.id}
teamSlug={params.team_slug}
/>
<div>
<AccountAbstractionSummary
teamId={project.teamId}
projectId={project.id}
/>
<div className="h-10" />
<AccountAbstractionAnalytics
userOpStats={userOpStats}
client={getThirdwebClient(authToken)}
teamId={project.teamId}
projectId={project.id}
teamSlug={params.team_slug}
/>
</div>
);
}
@@ -1,176 +1,80 @@
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ArrowRightIcon } from "lucide-react";
import { SupportedPlatformLink } from "../../../../../../../components/wallets/SupportedPlatformLink";
import { FooterLinksSection } from "../../../components/footer/FooterLinksSection";
export function InAppWalletFooterSection(props: {
trackingCategory: string;
}) {
export function InAppWalletsFooter() {
return (
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
<ViewDocs trackingCategory={props.trackingCategory} />
<Templates trackingCategory={props.trackingCategory} />
</div>
);
}
function ViewDocs(props: {
trackingCategory: string;
}) {
const TRACKING_CATEGORY = props.trackingCategory;
return (
<div className="rounded-lg border border-border bg-card p-4 lg:p-6">
<div className="flex items-center gap-2">
<h3 className="font-semibold">View Docs</h3>
<ArrowRightIcon className="size-4" />
</div>
<div className="h-4" />
<div className="grid grid-cols-2 gap-4">
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
platform="React"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
platform="Unity"
href="https://portal.thirdweb.com/unity/wallets/providers/embedded-wallet"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
platform="React Native"
href="https://portal.thirdweb.com/react/v5/in-app-wallet/get-started"
/>
<SupportedPlatformLink
trackingCategory={TRACKING_CATEGORY}
platform="TypeScript"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
/>
</div>
<div className="h-6" />
<div className="flex items-center gap-2">
<h3 className="font-semibold">Relevant Guides</h3>
<ArrowRightIcon className="size-4" />
</div>
<div className="h-4" />
<div className="flex flex-col gap-3">
<GuideLink
href="https://blog.thirdweb.com/what-are-embedded-wallets/"
label="what-is-an-embedded-wallet"
trackingCategory={TRACKING_CATEGORY}
>
What is an in-app wallet?
</GuideLink>
<GuideLink
href="https://portal.thirdweb.com/connect/in-app-wallet/get-started"
label="sdks-get-started"
trackingCategory={TRACKING_CATEGORY}
>
Get started with In-App Wallets
</GuideLink>
<GuideLink
href="https://portal.thirdweb.com/connect/in-app-wallet/how-to/connect-users"
label="how-to-connect-your-users"
trackingCategory={TRACKING_CATEGORY}
>
Using In-App Wallets with Connect
</GuideLink>
<GuideLink
href="https://portal.thirdweb.com/connect/in-app-wallet/how-to/build-your-own-ui"
label="how-to-build-your-own-ui"
trackingCategory={TRACKING_CATEGORY}
>
How to Build Your Own UI
</GuideLink>
<GuideLink
href="https://portal.thirdweb.com/connect/in-app-wallet/custom-auth/custom-auth-server"
label="how-to-custom-auth-server"
trackingCategory={TRACKING_CATEGORY}
>
Create a custom auth server
</GuideLink>
</div>
</div>
);
}
function GuideLink(props: {
label: string;
children: React.ReactNode;
href: string;
trackingCategory: string;
}) {
return (
<TrackedLinkTW
category={props.trackingCategory}
label="guide"
trackingProps={{
guide: props.label,
<FooterLinksSection
trackingCategory="in-app-wallets"
left={{
title: "Documentation",
links: [
{
href: "https://portal.thirdweb.com/connect/wallet/overview",
label: "Overview",
},
{
href: "https://portal.thirdweb.com/typescript/v5/in-app-wallet",
label: "Typescript",
},
{
href: "https://portal.thirdweb.com/react/v5/in-app-wallet/get-started",
label: "React",
},
{
href: "https://portal.thirdweb.com/react/v5/in-app-wallet/get-started",
label: "React Native",
},
{
href: "https://portal.thirdweb.com/dotnet/wallets/providers/in-app-wallet",
label: ".NET",
},
{
href: "https://portal.thirdweb.com/unity/v5/wallets/in-app-wallet",
label: "Unity",
},
{
href: "https://portal.thirdweb.com/unreal-engine/blueprints/in-app-wallet",
label: "Unreal Engine",
},
],
}}
href={props.href}
className="!text-muted-foreground hover:!text-foreground text-sm"
target="_blank"
>
{props.children}
</TrackedLinkTW>
);
}
function Templates(props: {
trackingCategory: string;
}) {
return (
<div className="rounded-lg border border-border bg-card p-4 lg:p-6">
<div className="flex items-center gap-2">
<h3 className="font-semibold">Relevant Templates</h3>
<ArrowRightIcon className="size-4" />
</div>
<div className="h-6" />
<div className="flex flex-col gap-3">
<GuideLink
href="https://github.com/thirdweb-example/embedded-smart-wallet"
label="embedded-smart-wallet"
trackingCategory={props.trackingCategory}
>
In-App Wallet + Account Abstraction Starter Kit
</GuideLink>
<GuideLink
href="https://github.com/thirdweb-example/catattacknft"
label="embedded-cat-attack"
trackingCategory={props.trackingCategory}
>
Cat Attack [Demo Web Game]
</GuideLink>
<GuideLink
href="https://github.com/thirdweb-example/embedded-wallet-custom-ui"
label="embedded-wallet-with-custom-ui-react"
trackingCategory={props.trackingCategory}
>
In-App Wallet With Custom UI [React]
</GuideLink>
<GuideLink
href="https://github.com/thirdweb-example/embedded-wallet-custom-ui-react-native"
label="embedded-wallet-with-custom-ui-react-native"
trackingCategory={props.trackingCategory}
>
In-App Wallet With Custom UI [ReactNative]
</GuideLink>
</div>
</div>
center={{
title: "Tutorials",
links: [
{
href: "https://www.youtube.com/watch?v=9s3Z-MvPv2I",
label: "Build your own custom connect component",
},
{
label: "Learn to create wallets inside a Unity game",
href: "https://www.youtube.com/watch?v=oUouNueoVFU",
},
{
href: "https://www.youtube.com/watch?v=xEp-fKba_cI",
label: "Pre-generate wallets for users",
},
{
label: "Sign-in with guest mode",
href: "https://portal.thirdweb.com/connect/wallet/sign-in-methods/guest",
},
],
}}
right={{
title: "Demos",
links: [
{
href: "https://playground.thirdweb.com/connect/in-app-wallet/ecosystem",
label: "Ecosystems",
},
{
href: "https://playground.thirdweb.com/connect/in-app-wallet/sponsor",
label: "Signless Sponsored Transactions",
},
{
href: "https://playground.thirdweb.com/connect/in-app-wallet",
label: "Customized UI",
},
],
}}
/>
);
}
@@ -1,43 +1,16 @@
import { getInAppWalletUsage } from "@/api/analytics";
import { TrackedUnderlineLink } from "@/components/ui/tracked-link";
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
import { subDays } from "date-fns";
import { TRACKING_CATEGORY } from "../_constants";
export async function InAppWalletsHeader({
teamId,
projectId,
}: { teamId: string; projectId: string }) {
const allTimeStatsPromise = getInAppWalletUsage({
teamId,
projectId,
from: new Date(2022, 0, 1),
to: new Date(),
period: "all",
});
const monthlyStatsPromise = getInAppWalletUsage({
teamId,
projectId,
from: subDays(new Date(), 30),
to: new Date(),
period: "month",
});
const [allTimeStats, monthlyStats] = await Promise.all([
allTimeStatsPromise,
monthlyStatsPromise,
]).catch(() => [null, null]);
export async function InAppWalletsHeader() {
return (
<div>
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:mb-2 lg:text-3xl">
<div className="">
<h1 className="mb-2 font-semibold text-2xl tracking-tighter lg:text-3xl">
In-App Wallets
</h1>
<p className="mb-7 max-w-[700px] text-muted-foreground text-sm leading-relaxed">
<p className="max-w-2xl text-muted-foreground text-sm leading-relaxed">
A wallet infrastructure that enables apps to create, manage, and control
their users wallets. Email login, social login, and bring-your-own auth
supported.{" "}
their users wallets. <br /> Email login, social login, and
bring-your-own auth supported.{" "}
<TrackedUnderlineLink
target="_blank"
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
@@ -47,10 +20,6 @@ export async function InAppWalletsHeader({
Learn more
</TrackedUnderlineLink>
</p>
<InAppWalletsSummary
allTimeStats={allTimeStats || []}
monthlyStats={monthlyStats || []}
/>
</div>
);
}
@@ -1,9 +1,8 @@
import { getProject } from "@/api/projects";
import { TabPathLinks } from "@/components/ui/tabs";
import { redirect } from "next/navigation";
import { InAppWalletFooterSection } from "./_components/footer";
import { InAppWalletsFooter } from "./_components/footer";
import { InAppWalletsHeader } from "./_components/header";
import { TRACKING_CATEGORY } from "./_constants";
export default async function Layout(props: {
params: Promise<{
@@ -22,34 +21,48 @@ export default async function Layout(props: {
const { team_slug, project_slug } = params;
return (
<div>
<InAppWalletsHeader teamId={project.teamId} projectId={project.id} />
<div className="h-8" />
<div className="flex grow flex-col">
{/* header */}
<div className="pt-4 lg:pt-6">
<div className="container max-w-7xl">
<InAppWalletsHeader />
</div>
<div className="h-3" />
<TabPathLinks
scrollableClassName="container max-w-7xl"
links={[
{
name: "Analytics",
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
exactMatch: true,
},
{
name: "Users",
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/users`,
exactMatch: true,
},
{
name: "Settings",
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/settings`,
exactMatch: true,
},
]}
/>
</div>
<TabPathLinks
links={[
{
name: "Analytics",
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
exactMatch: true,
},
{
name: "Users",
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/users`,
exactMatch: true,
},
{
name: "Settings",
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/settings`,
exactMatch: true,
},
]}
/>
{/* content */}
<div className="container flex max-w-7xl grow flex-col">
<div className="h-6" />
{props.children}
<div className="h-20" />
</div>
<div className="h-8" />
{props.children}
<div className="h-8" />
<InAppWalletFooterSection trackingCategory={TRACKING_CATEGORY} />
{/* footer */}
<div className="border-t">
<div className="container max-w-7xl">
<InAppWalletsFooter />
</div>
</div>
</div>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,7 +1,8 @@
import { getProject } from "@/api/projects";
import type { Range } from "components/analytics/date-range-selector";
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
import { redirect } from "next/navigation";
import { getProject } from "../../../../../../@/api/projects";
import { InAppWalletsSummary } from "../../../../../../components/embedded-wallets/Analytics/Summary";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
@@ -39,11 +40,15 @@ export default async function Page(props: {
}
return (
<InAppWalletAnalytics
teamId={project.teamId}
projectId={project.id}
interval={interval}
range={range as Range}
/>
<div>
<InAppWalletsSummary teamId={project.teamId} projectId={project.id} />
<div className="h-10" />
<InAppWalletAnalytics
teamId={project.teamId}
projectId={project.id}
interval={interval}
range={range as Range}
/>
</div>
);
}
@@ -1,14 +0,0 @@
import { redirect } from "next/navigation";
export default async function Page(props: {
params: Promise<{
team_slug: string;
project_slug: string;
}>;
}) {
const params = await props.params;
redirect(
`/team/${params.team_slug}/${params.project_slug}/connect/in-app-wallets`,
);
}
@@ -1,7 +1,8 @@
import { getProject } from "@/api/projects";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { TabPathLinks } from "@/components/ui/tabs";
import { redirect } from "next/navigation";
import { UnderlineLink } from "../../../../../../@/components/ui/UnderlineLink";
import { FooterLinksSection } from "../../components/footer/FooterLinksSection";
export default async function Layout(props: {
params: Promise<{
@@ -20,13 +21,13 @@ export default async function Layout(props: {
const payLayoutPath = `/team/${params.team_slug}/${params.project_slug}/connect/universal-bridge`;
return (
<div className="flex w-full flex-col gap-6">
<div className="flex flex-col items-start justify-between gap-6 lg:flex-row">
<div className="max-w-[700px]">
<div className="flex grow flex-col">
<div className="pt-4 lg:pt-6">
<div className="container max-w-7xl">
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:mb-2 lg:text-3xl">
Universal Bridge
</h1>
<p className="text-muted-foreground text-sm leading-relaxed">
<p className="max-w-3xl text-muted-foreground text-sm leading-relaxed">
Universal Bridge allows your users to bridge, swap, and purchase
cryptocurrencies and execute transactions with any fiat options or
tokens via cross-chain routing.{" "}
@@ -38,28 +39,99 @@ export default async function Layout(props: {
</UnderlineLink>
</p>
</div>
<div className="h-4" />
<TabPathLinks
scrollableClassName="container max-w-7xl"
className="w-full"
links={[
{
name: "Analytics",
path: payLayoutPath,
exactMatch: true,
},
{
name: "Webhooks",
path: `${payLayoutPath}/webhooks`,
},
{
name: "Settings",
path: `${payLayoutPath}/settings`,
},
]}
/>
</div>
<TabPathLinks
className="w-full"
links={[
{
name: "Analytics",
path: payLayoutPath,
exactMatch: true,
},
{
name: "Webhooks",
path: `${payLayoutPath}/webhooks`,
},
{
name: "Settings",
path: `${payLayoutPath}/settings`,
},
]}
/>
<div className="h-6" />
<div className="container flex max-w-7xl grow flex-col">
{props.children}
</div>
{props.children}
<div className="h-20" />
<div className="border-border border-t">
<div className="container max-w-7xl">
<UBFooter />
</div>
</div>
</div>
);
}
function UBFooter() {
return (
<FooterLinksSection
trackingCategory="universal-bridge"
left={{
title: "Documentation",
links: [
{
href: "https://portal.thirdweb.com/connect/pay/overview",
label: "Overview",
},
{
href: "https://portal.thirdweb.com/typescript/v5/convertCryptoToFiat",
label: "TypeScript",
},
{
href: "https://portal.thirdweb.com/react/v5/pay/fund-wallets",
label: "React",
},
{
href: "https://portal.thirdweb.com/dotnet/universal-bridge/quickstart",
label: ".NET",
},
],
}}
right={{
title: "Tutorials",
links: [
{
label: "Implement cross-chain payments in any app",
href: "https://www.youtube.com/watch?v=aBu175-VsNY",
},
],
}}
center={{
title: "Demos",
links: [
{
label: "UI Component",
href: "https://playground.thirdweb.com/connect/pay",
},
{
label: "Buy Crypto",
href: "https://playground.thirdweb.com/connect/pay/fund-wallet",
},
{
label: "Commerce",
href: "https://playground.thirdweb.com/connect/pay/commerce",
},
{
label: "Transactions",
href: "https://playground.thirdweb.com/connect/pay/transactions",
},
],
}}
/>
);
}
@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
import { DeployedContractsPage } from "../../../../account/contracts/_components/DeployedContractsPage";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../login/loginRedirect";
import { FooterLinksSection } from "../components/footer/FooterLinksSection";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
@@ -29,10 +30,71 @@ export default async function Page(props: {
}
return (
<DeployedContractsPage
teamId={team.id}
projectId={project.id}
authToken={authToken}
/>
<div className="flex grow flex-col">
<DeployedContractsPage
teamId={team.id}
projectId={project.id}
authToken={authToken}
/>
<div className="h-20" />
<ContractsFooter />
</div>
);
}
function ContractsFooter() {
return (
<div className="border-t">
<div className="container max-w-7xl">
<FooterLinksSection
trackingCategory="contracts"
left={{
title: "Documentation",
links: [
{
label: "Deployment Tools",
href: "https://portal.thirdweb.com/contracts/deploy/overview",
},
{
label: "Modular Contracts",
href: "https://portal.thirdweb.com/contracts/modular-contracts/overview",
},
{
label: "Pre-built Contracts",
href: "https://portal.thirdweb.com/contracts/explore/overview",
},
],
}}
center={{
title: "Templates",
links: [
{
label: "Hardhat Starter",
href: "https://thirdweb.com/templates/hardhat-starter",
},
{
href: "https://thirdweb.com/templates/forge-starter",
label: "Forge Starter",
},
],
}}
right={{
title: "Tutorials",
links: [
{
label:
"Everything you need to know about upgradeable smart contracts",
href: "https://www.youtube.com/watch?v=cZt-CkzxrNM",
},
{
label:
"Modular Contracts SDK: Build Core & Modules from Scratch (Advanced Guide)",
href: "https://www.youtube.com/watch?v=ZoOk41y4f_k",
},
],
}}
/>
</div>
</div>
);
}
@@ -0,0 +1,92 @@
import { Button } from "@/components/ui/button";
import {
Code2Icon,
DatabaseIcon,
ExternalLinkIcon,
ZapIcon,
} from "lucide-react";
import Link from "next/link";
export function BlueprintCard() {
const features = [
{
icon: Code2Icon,
title: "Easy-to-Use API",
description: "RESTful endpoints for any application",
},
{
icon: DatabaseIcon,
title: "Managed Infrastructure",
description:
"No need to index blockchains yourself or manage infrastructure and RPC costs.",
},
{
icon: ZapIcon,
title: "Lightning-Fast Queries",
description: "Access any transaction, event or token API data",
},
];
return (
<div className="rounded-lg border bg-card">
{/* header */}
<div className="border-b p-4 lg:px-6 lg:py-4">
<div className="flex items-center justify-between">
<h2 className="font-semibold text-2xl tracking-tight">Blueprints</h2>
<div className="flex items-center gap-2">
<Button asChild variant="outline" className="gap-2 bg-background">
<Link
href="https://portal.thirdweb.com/insight/blueprints"
target="_blank"
>
Docs <ExternalLinkIcon className="size-4" />
</Link>
</Button>
</div>
</div>
</div>
{/* Content */}
<div className="p-4 lg:p-6">
<p className="mb-2 font-semibold text-xl leading-tight tracking-tight lg:mb-1">
Simple endpoints for querying rich blockchain data
</p>
<p className="text-muted-foreground text-sm">
A blueprint is an API that provides access to on-chain data in a
user-friendly format. <br /> No need for ABIs, decoding, RPC, or web3
knowledge required to fetch blockchain data.
</p>
<div className="h-6" />
{/* Features */}
<div className="flex flex-col gap-6">
{features.map((feature) => (
<div key={feature.title} className="flex items-start gap-3">
<div className="rounded-full border p-2">
<feature.icon className="size-4 text-muted-foreground" />
</div>
<div>
<h4 className="font-medium">{feature.title}</h4>
<p className="text-muted-foreground text-sm">
{feature.description}
</p>
</div>
</div>
))}
</div>
</div>
{/* Playground link */}
<div className="border-t p-4 lg:p-6">
<Button className="w-full gap-2" asChild>
<Link href="https://playground.thirdweb.com/insight" target="_blank">
Try Insight blueprints in the playground
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
</div>
</div>
);
}

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