Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
b0b3b7f8f6 Version Packages (#6433)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-06 16:58:32 -08:00
Joaquim VergesandGitHub 617fe68c32 [SDK] feat: Allow custom storage for inApp and ecosystem wallets (#6432) 2025-03-06 16:16:32 -08:00
saminacodes 01d0666208 [Portal] Renames (#6430)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the terminology and references in the codebase from `thirdweb Pay` to `thirdweb Universal Bridge`, reflecting a rebranding and restructuring of the payment system.

### Detailed summary
- Updated sidebar links, changing "Testing Pay" to "Developer Mode" and renamed "Guides" to "Tutorials".
- Replaced instances of `thirdweb Pay` with `thirdweb Universal Bridge` in multiple files.
- Updated metadata titles and descriptions across various `.mdx` files.
- Modified FAQs to reflect new terminology for the Universal Bridge service.
- Adjusted documentation on customization and integration guides to use the new branding.

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

<!-- end pr-codex -->
2025-03-06 20:53:34 +00:00
MananTank 621357a5c6 [TOOL-3635] Dashboard: Fix AA factories page crash (#6428) 2025-03-06 16:11:27 +00:00
MananTank 0527ee26b7 [TOOL-3630] Dashboard: Add cancel subscriptions option in delete account flow (#6429) 2025-03-06 15:59:30 +00:00
Waren GonzagaandGitHub 834ed8337b Update page.mdx (#6425)
Signed-off-by: Waren Gonzaga <[email protected]>
2025-03-06 02:44:50 -08:00
Phillip HoandGitHub 11bb4f5fee chore: update mainnet AA message (#6427) 2025-03-06 02:44:28 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c547164320 Version Packages (#6417)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-05 17:37:39 -08:00
Joaquim VergesandGitHub 38ae2d3c6b [SDK] expose getUserOpHash utility function (#6424) 2025-03-05 17:08:15 -08:00
jnsdls d75320191d update ecosystem API routes to include team slug (#6422)
Updates ecosystem API endpoints to be scoped under team routes

The ecosystem wallet API endpoints have been updated to be scoped under team routes, ensuring proper data isolation between teams. All ecosystem-related API calls now include the team ID or slug in the path.

Key changes:
- Updated API endpoints from `/v1/ecosystem-wallet/*` to `/v1/teams/{teamIdOrSlug}/ecosystem-wallet/*`
- Added team slug parameter to ecosystem fetch and list operations
- Modified React hooks to include team context in query keys
- Updated component props to pass team information through the component tree

This change ensures ecosystem wallets are properly scoped to their respective teams and maintains data isolation between different team contexts.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates various functions and components within the ecosystem module to include `team_slug` as a parameter. This change enhances the ability to fetch ecosystems and their details specific to a team, improving data handling across the application.

### Detailed summary
- Updated `fetchEcosystem` to accept `team_slug`.
- Modified `fetchEcosystemList` to include `team_slug`.
- Adjusted the `useEcosystemList` hook to utilize `teamIdOrSlug`.
- Enhanced `useEcosystem` to accept `teamIdOrSlug`.
- Updated components to pass `team_slug` where necessary.
- Changed redirect logic to incorporate team-specific ecosystems.

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

<!-- end pr-codex -->
2025-03-05 19:00:54 +00:00
saminacodes 6046d4510d [Portal] Universal Bridge Docs (#6421)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming and updating the documentation related to `thirdweb Pay`, transitioning to the `Universal Bridge` terminology. It enhances clarity and alignment with the new features and guides for web3 payments.

### Detailed summary
- Updated titles and descriptions in various `.mdx` files to reflect `Universal Bridge`.
- Changed sidebar navigation names and links to align with `Universal Bridge`.
- Revised content in `page.mdx` files to include new functionalities and guides.
- Added sections on fee sharing and supported chains in the `Overview` documentation.
- Modified FAQ entries to clarify `Universal Bridge` specifics.

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

<!-- end pr-codex -->
2025-03-05 18:28:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f1d32261f0 chore(deps-dev): bump eslint-plugin-react-compiler from 19.0.0-beta-e1e972c-20250221 to 19.0.0-beta-40c6c23-20250301 (#6395)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 09:19:51 -08:00
Manan TankandGitHub 04baa74ad7 [TOOL-3610] Dashboard: Fix z-index issue with network selector on chakra modal (#6418) 2025-03-05 07:51:33 -08:00
Phillip HoandGitHub 44bb0034a6 [service-utils] Add pay as a usage source (#6416) 2025-03-05 18:51:39 +08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
cfeba37e40 Version Packages (#6410)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-05 02:15:40 -08:00
Joaquim VergesandGitHub fb446bd099 [SDK] feat: Add transfer success callback (#6415) 2025-03-05 02:09:02 -08:00
jnsdls 24063af948 update engine API routes to include team slug (#6407)
blocked by: https://app.graphite.dev/github/pr/thirdweb-dev/api-server/1437/add-team-engine-management-endpoints

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of `teamIdOrSlug` across various components and utility functions related to engine instances and alerts, ensuring that the correct team context is maintained throughout the application.

### Detailed summary
- Added `teamIdOrSlug` parameter to multiple functions and components.
- Updated API endpoints to include `teamIdOrSlug` in fetch calls.
- Modified components like `EngineAlertsPage`, `EngineSystem`, and `EngineInstancesTable` to pass `teamIdOrSlug`.
- Adjusted data structure handling in API responses to use `result` instead of `data`.

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

<!-- end pr-codex -->
2025-03-05 01:39:38 +00:00
MananTank e2c9d4de34 [TOOL-3606] playground: Remove Ethereum as default selected chain in insight playground (#6413)
<!-- start pr-codex -->

## PR-Codex overview
This PR refines the logic for setting default values in the `values` object within the `blueprint-playground.client.tsx` file. It consolidates the conditions for assigning a default value to parameters.

### Detailed summary
- Removed the specific check for `param.name === "chain"` that assigned a hardcoded value of `"1"`.
- Simplified the condition to check if `param.schema` has a `type` and a `default` property.
- Updated the assignment of `values[param.name]` to use `param.schema.default` when applicable, otherwise setting it to an empty string.

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

<!-- end pr-codex -->
2025-03-05 01:14:05 +00:00
jnsdls 0f6aa6b85d add /faucets to framer rewrites (#6414)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on adding a new route to the `framer-rewrites.js` file for redirecting to the `/faucets` page.

### Detailed summary
- Added the route `"/faucets"` to the array of paths in `apps/dashboard/framer-rewrites.js`.

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

<!-- end pr-codex -->
2025-03-05 01:06:38 +00:00
Joaquim VergesandGitHub 9f1a77f4c4 [SDK] fix: Allow resending OTP on failures (#6412) 2025-03-04 16:58:30 -08:00
Manan TankandGitHub 158a08aafc [TOOL-3598] Dashboard: Add error message in create ecosystem form for image size (#6411) 2025-03-04 12:16:25 -08:00
Phillip HoandGitHub 67354dc848 [service-utils] remove unneeded cf worker imports (#6409) 2025-03-04 21:57:02 +08:00
Joaquim VergesandGitHub 575b7b3a58 [SDK] feat: update zksync chain checks (#6408) 2025-03-04 01:50:43 -08:00
Jonas DanielsandGitHub 35ccf8f2e8 updated dependencies (#6404) 2025-03-03 20:19:54 -08:00
MananTank b5e02a4f10 [TOOl-3590] Dashboard: Remove unused components and stories (#6406)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on removing unused components and simplifying the `CouponCard` functionality by streamlining the handling of coupon applications and eliminating the modal for sharing free wallets.

### Detailed summary
- Deleted multiple files related to `share-free-wallets-modal` and analytics components.
- Removed the dynamic import of `LazyShareFreeWalletsModal` in `CouponCard.tsx`.
- Simplified the `onCouponApplied` function signature by removing the `isFreeWalletsCoupon` parameter.
- Eliminated state management for `showShareModal` in `CouponSection`.

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

<!-- end pr-codex -->
2025-03-04 01:47:57 +00:00
kumaryash90 cb71dd597e Update implementations (#6405)
<!--

## 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 the `MarketplaceV3` addresses in the `ZKSYNC_IMPLEMENTATIONS` object for various entries to new contract addresses.

### Detailed summary
- Updated `MarketplaceV3` address from `0xBc02441a36Bb4029Cd191b20243c2e41B862F118` to `0x88e7F2bEf0a23073b7Cc2c79D307Dbd9138Fa79c` at index `324`.
- Updated `MarketplaceV3` address from `0x4027561E163a420c4e5Db46E07EBd581CAa8Bb62` to `0x4b14569c7B79DBe686Ac3Ba5996131E7EDaB7a93` at index `2741`.

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

<!-- end pr-codex -->
2025-03-04 00:03:26 +00:00
Alec AnanianandGitHub 83ec6957f9 playground: fix pay example imports (#6391) 2025-03-03 15:56:36 -08:00
kumaryash90 c0bbc21a84 Update deploy configs (#6299)
TOOL-3478

<!--

## 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 platform fee configuration in the contract deployment process, introducing a new fee constant, and modifying the logic to handle different fee scenarios based on the publisher address.

### Detailed summary
- Added `THIRDWEB_PUBLISHER_ADDRESS` constant in `addresses.ts`.
- Introduced `DEFAULT_FEE_BPS_NEW` constant in `addresses.ts`.
- Updated `PlatformFeeFieldset` to destructure `isMarketplace`.
- Modified `CustomContractForm` to conditionally use `DEFAULT_FEE_BPS_NEW` based on the publisher address.
- Adjusted imports in `custom-contract.tsx` to include new constants.

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

<!-- end pr-codex -->
2025-03-03 22:09:24 +00:00
Jonas DanielsandGitHub 9fcf1b80de skip smart wallet tests without TW_SECRET_KEY (#6403) 2025-03-03 13:43:07 -08:00
Jonas DanielsandGitHub 33ddf56a0a remove secretHash from rotate key response (#6401) 2025-03-03 13:32:38 -08:00
MananTank f9fa5df6b1 [TOOl-3587] Dashboard: Add in-app-wallet login mode (#6400)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces enhancements to the `LoginAndOnboardingPage` by adding support for an in-app wallet login option and refactoring the login handling logic.

### Detailed summary
- Added `loginWithInAppWallet` prop to `LoginAndOnboardingPage` and `LoginAndOnboardingPageContent`.
- Updated `Page` component to handle `in-app-wallet` search parameter.
- Introduced `inAppWalletLoginOptions` for in-app wallet authentication.
- Modified wallet selection logic based on `loginWithInAppWallet` prop.

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

<!-- end pr-codex -->
2025-03-03 21:17:31 +00:00
MananTank 0d82fc0dd1 [TOOL-3584] Dashboad: Move turnstile widget above connect embed in login page (#6394) 2025-03-03 20:21:59 +00:00
MananTank ef6029b939 [TOOL-3575] Dashboard: Fix /home link 404 after upgrading to nextjs 15.2 (#6393)
<!--

## 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 routing and links in the dashboard application to improve navigation.

### Detailed summary
- In `apps/dashboard/next.config.ts`, the `destination` was changed from `"/"` to `"https://landing.thirdweb.com"`.
- In `apps/dashboard/src/app/components/MobileBurgerMenuButton.tsx`, the `href` for the `Link` component was updated from `"/"` to `"/home"`.

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

<!-- end pr-codex -->
2025-03-03 19:56:41 +00:00
MananTank 004601812a [TOOL-3581] Dashboard: Fix project overview page crashing because of ContractDistributionCard (#6392) 2025-03-03 18:39:45 +00:00
126 changed files with 3176 additions and 4416 deletions
+2
View File
@@ -55,4 +55,6 @@ module.exports = [
"/learn/guides/:guide_slug",
"/learn/courses",
"/learn/glossary",
// -- faucets --
"/faucets",
];
+1 -1
View File
@@ -153,7 +153,7 @@ const baseNextConfig: NextConfig = {
// re-write /home to / (this is so that logged in users will be able to go to /home and NOT be redirected to the logged in app)
{
source: "/home",
destination: "/",
destination: "https://landing.thirdweb.com",
},
...FRAMER_PATHS.map((path) => ({
source: path,
+21 -21
View File
@@ -45,9 +45,9 @@
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tooltip": "1.1.8",
"@sentry/nextjs": "9.2.0",
"@shazow/whatsabi": "^0.19.0",
"@tanstack/react-query": "5.66.9",
"@sentry/nextjs": "9.3.0",
"@shazow/whatsabi": "0.20.0",
"@tanstack/react-query": "5.67.1",
"@tanstack/react-table": "^8.21.2",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "2.0.0",
@@ -61,13 +61,13 @@
"date-fns": "4.1.0",
"fetch-event-stream": "0.1.5",
"flat": "^6.0.1",
"framer-motion": "12.4.7",
"framer-motion": "12.4.10",
"fuse.js": "7.1.0",
"idb-keyval": "^6.2.1",
"input-otp": "^1.4.1",
"ioredis": "^5.5.0",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.476.0",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.4",
@@ -94,7 +94,7 @@
"shiki": "1.27.0",
"sonner": "2.0.1",
"spdx-correct": "^3.2.0",
"swagger-ui-react": "^5.19.0",
"swagger-ui-react": "^5.20.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
@@ -104,21 +104,21 @@
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.4",
"@chromatic-com/storybook": "3.2.5",
"@next/bundle-analyzer": "15.2.0",
"@next/eslint-plugin-next": "15.2.0",
"@playwright/test": "1.50.1",
"@storybook/addon-essentials": "8.6.0",
"@storybook/addon-interactions": "8.6.0",
"@storybook/addon-links": "8.6.0",
"@storybook/addon-onboarding": "8.6.0",
"@storybook/addon-viewport": "8.6.0",
"@storybook/blocks": "8.6.0",
"@storybook/nextjs": "8.6.0",
"@storybook/react": "8.6.0",
"@storybook/test": "8.6.0",
"@storybook/addon-essentials": "8.6.3",
"@storybook/addon-interactions": "8.6.3",
"@storybook/addon-links": "8.6.3",
"@storybook/addon-onboarding": "8.6.3",
"@storybook/addon-viewport": "8.6.3",
"@storybook/blocks": "8.6.3",
"@storybook/nextjs": "8.6.3",
"@storybook/react": "8.6.3",
"@storybook/test": "8.6.3",
"@types/color": "4.2.0",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
@@ -130,16 +130,16 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.19",
"checkly": "^4.19.1",
"checkly": "5.0.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"eslint-plugin-storybook": "0.11.3",
"knip": "5.45.0",
"next-sitemap": "^4.2.3",
"postcss": "8.5.3",
"storybook": "8.6.0",
"storybook": "8.6.3",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
+7
View File
@@ -172,3 +172,10 @@ input:-webkit-autofill:active {
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
/* Remove this when chakra is removed */
/* Make chakra modals have same z-index as shadcn dialog */
body {
--chakra-zIndices-modal: 50;
--chakra-zIndices-overlay: 50;
}
@@ -371,7 +371,6 @@ export type RotateSecretKeyAPIReturnType = {
data: {
secret: string;
secretMasked: string;
secretHash: string;
};
};
@@ -232,13 +232,15 @@ export function useEngineUpdateDeployment() {
export type RemoveEngineFromDashboardIParams = {
instanceId: string;
teamIdOrSlug: string;
};
export async function removeEngineFromDashboard({
instanceId,
teamIdOrSlug,
}: RemoveEngineFromDashboardIParams) {
const res = await apiServerProxy({
pathname: `/v1/engine/${instanceId}`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${instanceId}`,
method: "DELETE",
});
@@ -273,18 +275,20 @@ export async function deleteCloudHostedEngine({
}
export type EditEngineInstanceParams = {
teamIdOrSlug: string;
instanceId: string;
name: string;
url: string;
};
export async function editEngineInstance({
teamIdOrSlug,
instanceId,
name,
url,
}: EditEngineInstanceParams) {
const res = await apiServerProxy({
pathname: `/v1/engine/${instanceId}`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${instanceId}`,
method: "PUT",
headers: {
"Content-Type": "application/json",
@@ -1628,7 +1632,7 @@ export function useEngineSubscriptionsLastBlock(params: {
interface EngineResourceMetrics {
error: string;
data: {
result: {
cpu: number;
memory: number;
errorRate: ResultItem[];
@@ -1637,7 +1641,7 @@ interface EngineResourceMetrics {
};
}
export function useEngineSystemMetrics(engineId: string) {
export function useEngineSystemMetrics(engineId: string, teamIdOrSlug: string) {
const [enabled, setEnabled] = useState(true);
return useQuery({
@@ -1645,7 +1649,7 @@ export function useEngineSystemMetrics(engineId: string) {
queryFn: async () => {
const res = await apiServerProxy({
method: "GET",
pathname: `/v1/engine/${engineId}/metrics`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/metrics`,
});
if (!res.ok) {
@@ -1674,14 +1678,14 @@ export interface EngineAlertRule {
pausedAt: Date | null;
}
export function useEngineAlertRules(engineId: string) {
export function useEngineAlertRules(engineId: string, teamIdOrSlug: string) {
return useQuery({
queryKey: engineKeys.alertRules(engineId),
queryFn: async () => {
const res = await apiServerProxy<{
data: EngineAlertRule[];
}>({
pathname: `/v1/engine/${engineId}/alert-rules`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/alert-rules`,
method: "GET",
});
@@ -1703,14 +1707,19 @@ export interface EngineAlert {
endsAt: Date | null;
}
export function useEngineAlerts(engineId: string, limit: number, offset = 0) {
export function useEngineAlerts(
engineId: string,
teamIdOrSlug: string,
limit: number,
offset = 0,
) {
return useQuery({
queryKey: engineKeys.alerts(engineId),
queryFn: async () => {
const res = await apiServerProxy<{
data: EngineAlert[];
}>({
pathname: `/v1/engine/${engineId}/alerts`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/alerts`,
searchParams: {
limit: `${limit}`,
offset: `${offset}`,
@@ -1754,14 +1763,17 @@ export type EngineNotificationChannel = {
subscriptionRoutes: string[];
};
export function useEngineNotificationChannels(engineId: string) {
export function useEngineNotificationChannels(
engineId: string,
teamIdOrSlug: string,
) {
return useQuery({
queryKey: engineKeys.notificationChannels(engineId),
queryFn: async () => {
const res = await apiServerProxy<{
data: EngineNotificationChannel[];
}>({
pathname: `/v1/engine/${engineId}/notification-channels`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels`,
method: "GET",
});
@@ -1781,7 +1793,10 @@ export interface CreateNotificationChannelInput {
value: string;
}
export function useEngineCreateNotificationChannel(engineId: string) {
export function useEngineCreateNotificationChannel(
engineId: string,
teamIdOrSlug: string,
) {
const queryClient = useQueryClient();
return useMutation({
@@ -1789,7 +1804,7 @@ export function useEngineCreateNotificationChannel(engineId: string) {
const res = await apiServerProxy<{
data: EngineNotificationChannel;
}>({
pathname: `/v1/engine/${engineId}/notification-channels`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels`,
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -1812,13 +1827,16 @@ export function useEngineCreateNotificationChannel(engineId: string) {
});
}
export function useEngineDeleteNotificationChannel(engineId: string) {
export function useEngineDeleteNotificationChannel(
engineId: string,
teamIdOrSlug: string,
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (notificationChannelId: string) => {
const res = await apiServerProxy({
pathname: `/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels/${notificationChannelId}`,
method: "DELETE",
});
@@ -36,6 +36,16 @@ export function AccountSettingsPage(props: {
client={props.client}
defaultTeamSlug={props.defaultTeamSlug}
defaultTeamName={props.defaultTeamName}
cancelSubscriptions={async () => {
const res = await apiServerProxy({
method: "DELETE",
pathname: `/v1/teams/${props.defaultTeamSlug}/subscriptions`,
});
if (!res.ok) {
throw new Error(res.error);
}
}}
onAccountDeleted={async () => {
await doLogout();
if (activeWallet) {
@@ -143,6 +143,9 @@ function Variants() {
onAccountDeleted={() => {
console.log("Account deleted");
}}
cancelSubscriptions={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
/>
<Toaster richColors />
</div>
@@ -38,7 +38,7 @@ import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { EllipsisIcon, ExternalLinkIcon } from "lucide-react";
import { CircleXIcon, EllipsisIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -64,6 +64,7 @@ export function AccountSettingsPageUI(props: {
defaultTeamSlug: string;
defaultTeamName: string;
redirectToBillingPortal: BillingBillingPortalAction;
cancelSubscriptions: () => Promise<void>;
}) {
return (
<div className="flex flex-col gap-8">
@@ -89,6 +90,7 @@ export function AccountSettingsPageUI(props: {
defaultTeamSlug={props.defaultTeamSlug}
defaultTeamName={props.defaultTeamName}
redirectToBillingPortal={props.redirectToBillingPortal}
cancelSubscriptions={props.cancelSubscriptions}
/>
</div>
);
@@ -207,6 +209,7 @@ function DeleteAccountCard(props: {
defaultTeamSlug: string;
defaultTeamName: string;
redirectToBillingPortal: BillingBillingPortalAction;
cancelSubscriptions: () => Promise<void>;
}) {
const title = "Delete Account";
const description = (
@@ -230,6 +233,10 @@ function DeleteAccountCard(props: {
},
});
const cancelSubscriptions = useMutation({
mutationFn: props.cancelSubscriptions,
});
function handleDelete() {
deleteAccount.mutate();
}
@@ -264,25 +271,47 @@ function DeleteAccountCard(props: {
<Alert variant="destructive">
<AlertTitle>Failed to delete account</AlertTitle>
<AlertDescription>
<span className="mb-1 block">
Your default team "{props.defaultTeamName}" has active
subscriptions. These subscriptions have to be cancelled
first before deleting account
</span>
<span className="mb-4 block">
Reach out to support to help you cancel them
</span>
<Button
variant="default"
asChild
size="sm"
className="gap-2"
>
<Link href="/support/create-ticket">
Contact Support
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
<div className="mb-4">
<span className="block">
Your default team "{props.defaultTeamName}" has active
subscriptions. These subscriptions have to be cancelled
before deleting account
</span>
</div>
<div className="flex flex-col gap-3 lg:flex-row">
<Button
variant="default"
size="sm"
className="gap-2"
onClick={() => {
const promise = cancelSubscriptions.mutateAsync();
toast.promise(promise, {
success: "Subscriptions cancelled successfully",
error: "Failed to cancel subscriptions",
});
}}
>
{cancelSubscriptions.isPending ? (
<Spinner className="size-4" />
) : (
<CircleXIcon className="size-4" />
)}
Cancel subscriptions
</Button>
<Button
variant="outline"
asChild
size="sm"
className="gap-2"
>
<Link href="/support/create-ticket" target="_blank">
Contact Support
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
</div>
</AlertDescription>
</Alert>
</div>
@@ -166,7 +166,7 @@ export function MobileBurgerMenuButton(
</Link>
<Link
href="/"
href="/home"
className="text-base text-muted-foreground hover:text-foreground"
>
Home Page
+43 -13
View File
@@ -24,7 +24,7 @@ const LazyOnboardingUI = lazy(
() => import("./onboarding/on-boarding-ui.client"),
);
const wallets = [
const loginOptions = [
inAppWallet({
auth: {
options: [
@@ -45,9 +45,27 @@ const wallets = [
createWallet("io.zerion.wallet"),
];
const inAppWalletLoginOptions = [
inAppWallet({
auth: {
options: [
"google",
"apple",
"facebook",
"github",
"email",
"phone",
"passkey",
"wallet",
],
},
}),
];
export function LoginAndOnboardingPage(props: {
account: Account | undefined;
redirectPath: string;
loginWithInAppWallet: boolean;
}) {
return (
<div className="relative flex min-h-dvh flex-col overflow-hidden bg-background">
@@ -91,6 +109,7 @@ export function LoginAndOnboardingPage(props: {
<LoginAndOnboardingPageContent
account={props.account}
redirectPath={props.redirectPath}
loginWithInAppWallet={props.loginWithInAppWallet}
/>
</div>
);
@@ -99,6 +118,7 @@ export function LoginAndOnboardingPage(props: {
export function LoginAndOnboardingPageContent(props: {
account: Account | undefined;
redirectPath: string;
loginWithInAppWallet: boolean;
}) {
return (
<div className="relative flex grow flex-col">
@@ -114,6 +134,7 @@ export function LoginAndOnboardingPageContent(props: {
<PageContent
redirectPath={props.redirectPath}
account={props.account}
loginWithInAppWallet={props.loginWithInAppWallet}
/>
</ClientOnly>
</main>
@@ -139,6 +160,7 @@ function LoadingCard() {
function PageContent(props: {
redirectPath: string;
account: Account | undefined;
loginWithInAppWallet: boolean;
}) {
const [screen, setScreen] = useState<
| { id: "login" }
@@ -190,7 +212,12 @@ function PageContent(props: {
}
if (connectionStatus !== "connected" || screen.id === "login") {
return <CustomConnectEmbed onLogin={onLogin} />;
return (
<CustomConnectEmbed
onLogin={onLogin}
loginWithInAppWallet={props.loginWithInAppWallet}
/>
);
}
if (screen.id === "onboarding") {
@@ -215,6 +242,7 @@ function PageContent(props: {
function CustomConnectEmbed(props: {
onLogin: () => void;
loginWithInAppWallet: boolean;
}) {
const { theme } = useTheme();
const client = useThirdwebClient();
@@ -222,6 +250,16 @@ function CustomConnectEmbed(props: {
return (
<div className="flex flex-col items-center gap-4">
<Turnstile
options={{
// only show if interaction is required
appearance: "interaction-only",
// match the theme of the rest of the app
theme: theme === "light" ? "light" : "dark",
}}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={(token) => setTurnstileToken(token)}
/>
<ConnectEmbed
auth={{
getLoginPayload,
@@ -247,7 +285,9 @@ function CustomConnectEmbed(props: {
return isLoggedInResult;
},
}}
wallets={wallets}
wallets={
props.loginWithInAppWallet ? inAppWalletLoginOptions : loginOptions
}
client={client}
modalSize="wide"
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
@@ -255,16 +295,6 @@ function CustomConnectEmbed(props: {
privacyPolicyUrl="/privacy-policy"
termsOfServiceUrl="/terms"
/>
<Turnstile
options={{
// only show if interaction is required
appearance: "interaction-only",
// match the theme of the rest of the app
theme: theme === "light" ? "light" : "dark",
}}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={(token) => setTurnstileToken(token)}
/>
</div>
);
}
+10 -3
View File
@@ -4,10 +4,13 @@ import { isValidEncodedRedirectPath } from "./isValidEncodedRedirectPath";
export default async function Page(props: {
searchParams: Promise<{
next?: string;
next: string | string[] | undefined;
"in-app-wallet": string | string[] | undefined;
}>;
}) {
const nextPath = (await props.searchParams).next;
const searchParams = await props.searchParams;
const nextPath =
typeof searchParams.next === "string" ? searchParams.next : undefined;
const account = await getRawAccount();
// don't redirect away from login page if authToken is already present and onboarding is done
@@ -20,6 +23,10 @@ export default async function Page(props: {
nextPath && isValidEncodedRedirectPath(nextPath) ? nextPath : "/team";
return (
<LoginAndOnboardingPage account={account} redirectPath={redirectPath} />
<LoginAndOnboardingPage
account={account}
redirectPath={redirectPath}
loginWithInAppWallet={searchParams["in-app-wallet"] === "true"}
/>
);
}
@@ -53,6 +53,7 @@ export function NebulaLoginPage(props: {
{showPage === "connect" && (
<LoginAndOnboardingPageContent
loginWithInAppWallet={false}
account={props.account}
redirectPath={
message ? `/?prompt=${encodeURIComponent(message)}` : "/"
@@ -28,7 +28,7 @@ export default async function Page(props: {
}
const [ecosystem, team] = await Promise.all([
fetchEcosystem(params.slug, authToken),
fetchEcosystem(params.slug, authToken, params.team_slug),
getTeamBySlug(params.team_slug),
]);
@@ -10,7 +10,7 @@ export async function EcosystemLayoutSlug({
ecosystemLayoutPath,
}: {
children: React.ReactNode;
params: { slug: string };
params: { slug: string; team_slug: string };
ecosystemLayoutPath: string;
}) {
const authToken = await getAuthToken();
@@ -19,7 +19,11 @@ export async function EcosystemLayoutSlug({
redirect(ecosystemLayoutPath);
}
const ecosystem = await fetchEcosystem(params.slug, authToken);
const ecosystem = await fetchEcosystem(
params.slug,
authToken,
params.team_slug,
);
if (!ecosystem) {
redirect(ecosystemLayoutPath);
@@ -30,6 +34,7 @@ export async function EcosystemLayoutSlug({
<EcosystemHeader
ecosystem={ecosystem}
ecosystemLayoutPath={ecosystemLayoutPath}
teamIdOrSlug={params.team_slug}
/>
<SidebarLayout
@@ -62,8 +62,11 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
function EcosystemSelect(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
}) {
const { data: ecosystems, isPending } = useEcosystemList();
const { data: ecosystems, isPending } = useEcosystemList({
teamIdOrSlug: props.teamIdOrSlug,
});
return isPending ? (
<Skeleton className="h-10 w-full md:w-[160px]" />
@@ -109,8 +112,10 @@ function EcosystemSelect(props: {
export function EcosystemHeader(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
}) {
const { data: fetchedEcosystem } = useEcosystem({
teamIdOrSlug: props.teamIdOrSlug,
slug: props.ecosystem.slug,
refetchInterval:
props.ecosystem.status === "requested"
@@ -192,6 +197,7 @@ export function EcosystemHeader(props: {
<EcosystemSelect
ecosystem={ecosystem}
ecosystemLayoutPath={props.ecosystemLayoutPath}
teamIdOrSlug={props.teamIdOrSlug}
/>
</div>
</div>
@@ -7,8 +7,11 @@ import { IntegrationPermissionsSection } from "../server/integration-permissions
export function EcosystemPermissionsPage({
params,
authToken,
}: { params: { slug: string }; authToken: string }) {
const { data: ecosystem } = useEcosystem({ slug: params.slug });
}: { params: { slug: string; team_slug: string }; authToken: string }) {
const { data: ecosystem } = useEcosystem({
slug: params.slug,
teamIdOrSlug: params.team_slug,
});
return (
<div className="flex flex-col gap-8">
@@ -3,21 +3,23 @@ import { useQuery } from "@tanstack/react-query";
import type { Ecosystem } from "../../../types";
export function useEcosystem({
teamIdOrSlug,
slug,
refetchInterval,
refetchOnWindowFocus,
initialData,
}: {
teamIdOrSlug: string;
slug: string;
refetchInterval?: number;
refetchOnWindowFocus?: boolean;
initialData?: Ecosystem;
}) {
return useQuery({
queryKey: ["ecosystems", slug],
queryKey: ["ecosystems", teamIdOrSlug, slug],
queryFn: async () => {
const res = await apiServerProxy({
pathname: `/v1/ecosystem-wallet/${slug}`,
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
method: "GET",
});
@@ -31,9 +31,13 @@ const formSchema = z.object({
.refine((name) => /^[a-zA-Z0-9 ]*$/.test(name), {
message: "Name can only contain letters, numbers and spaces",
}),
logo: z.instanceof(File, {
message: "Logo is required",
}),
logo: z
.instanceof(File, {
message: "Logo is required",
})
.refine((file) => file.size <= 500 * 1024, {
message: "Logo size must be less than 500KB",
}),
permission: z.union([z.literal("PARTNER_WHITELIST"), z.literal("ANYONE")]),
});
@@ -118,8 +122,9 @@ export function CreateEcosystemForm(props: { teamSlug: string }) {
accept="image/png, image/jpeg"
onUpload={(files) => {
if (files[0]) {
form.setValue("logo", files[0]);
form.clearErrors("logo");
form.setValue("logo", files[0], {
shouldValidate: true,
});
}
}}
/>
@@ -3,13 +3,17 @@ import { useQuery } from "@tanstack/react-query";
import { useActiveAccount } from "thirdweb/react";
import type { Ecosystem } from "../types";
export function useEcosystemList() {
export function useEcosystemList({
teamIdOrSlug,
}: {
teamIdOrSlug: string;
}) {
const address = useActiveAccount()?.address;
return useQuery({
queryKey: ["ecosystems", address],
queryKey: ["ecosystems", teamIdOrSlug, address],
queryFn: async () => {
const res = await apiServerProxy({
pathname: "/v1/ecosystem-wallet/list",
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
method: "GET",
});
@@ -20,10 +20,12 @@ export default async function Page(props: {
loginRedirect(ecosystemLayoutPath);
}
const ecosystems = await fetchEcosystemList(authToken).catch((err) => {
console.error("failed to fetch ecosystems", err);
return [];
});
const ecosystems = await fetchEcosystemList(authToken, team_slug).catch(
(err) => {
console.error("failed to fetch ecosystems", err);
return [];
},
);
if (ecosystems[0]) {
redirect(`${ecosystemLayoutPath}/${ecosystems[0].slug}`);
}
@@ -1,12 +1,19 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Ecosystem } from "../types";
export async function fetchEcosystem(slug: string, authToken: string) {
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/${slug}`, {
headers: {
Authorization: `Bearer ${authToken}`,
export async function fetchEcosystem(
slug: string,
authToken: string,
teamIdOrSlug: string,
) {
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
});
);
if (!res.ok) {
const data = await res.json();
console.error(data);
@@ -1,12 +1,18 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Ecosystem } from "../types";
export async function fetchEcosystemList(authToken: string) {
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/list`, {
headers: {
Authorization: `Bearer ${authToken}`,
export async function fetchEcosystemList(
authToken: string,
teamIdOrSlug: string,
) {
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
});
);
if (!res.ok) {
const data = await res.json();
@@ -28,12 +28,15 @@ const formSchema = z.object({
type ImportEngineParams = z.infer<typeof formSchema>;
async function importEngine(data: ImportEngineParams) {
async function importEngine({
teamIdOrSlug,
...data
}: ImportEngineParams & { teamIdOrSlug: string }) {
// Instance URLs should end with a /.
const url = data.url.endsWith("/") ? data.url : `${data.url}/`;
const res = await apiServerProxy({
pathname: "/v1/engine",
pathname: `/v1/teams/${teamIdOrSlug}/engine`,
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -59,7 +62,7 @@ export function EngineImportCard(props: {
<EngineImportCardUI
prefillImportUrl={props.prefillImportUrl}
importEngine={async (params) => {
await importEngine(params);
await importEngine({ ...params, teamIdOrSlug: props.teamSlug });
router.push(`/team/${props.teamSlug}/~/engine`);
}}
/>
@@ -74,6 +74,7 @@ type RemovedEngineFromDashboard = (
) => Promise<void>;
export function EngineInstancesTable(props: {
teamIdOrSlug: string;
instances: EngineInstance[];
engineLinkPrefix: string;
}) {
@@ -81,6 +82,7 @@ export function EngineInstancesTable(props: {
return (
<EngineInstancesTableUI
teamIdOrSlug={props.teamIdOrSlug}
instances={props.instances}
engineLinkPrefix={props.engineLinkPrefix}
deleteCloudHostedEngine={async (params) => {
@@ -100,6 +102,7 @@ export function EngineInstancesTable(props: {
}
export function EngineInstancesTableUI(props: {
teamIdOrSlug: string;
instances: EngineInstance[];
engineLinkPrefix: string;
deleteCloudHostedEngine: DeletedCloudHostedEngine;
@@ -124,6 +127,7 @@ export function EngineInstancesTableUI(props: {
{props.instances.map((instance) => (
<EngineInstanceRow
key={instance.id}
teamIdOrSlug={props.teamIdOrSlug}
instance={instance}
engineLinkPrefix={props.engineLinkPrefix}
deleteCloudHostedEngine={props.deleteCloudHostedEngine}
@@ -145,6 +149,7 @@ export function EngineInstancesTableUI(props: {
}
function EngineInstanceRow(props: {
teamIdOrSlug: string;
instance: EngineInstance;
engineLinkPrefix: string;
deleteCloudHostedEngine: DeletedCloudHostedEngine;
@@ -187,6 +192,7 @@ function EngineInstanceRow(props: {
</TableRow>
<EditModal
teamIdOrSlug={props.teamIdOrSlug}
instance={instance}
open={isEditModalOpen}
onOpenChange={setIsEditModalOpen}
@@ -194,6 +200,7 @@ function EngineInstanceRow(props: {
/>
<RemoveModal
teamIdOrSlug={props.teamIdOrSlug}
instance={instance}
onOpenChange={setIsRemoveModalOpen}
open={isRemoveModalOpen}
@@ -349,6 +356,7 @@ function EngineActionsDropdown(props: {
function EditModal(props: {
open: boolean;
teamIdOrSlug: string;
onOpenChange: (open: boolean) => void;
instance: EngineInstance;
editEngineInstance: EditedEngineInstance;
@@ -357,6 +365,7 @@ function EditModal(props: {
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className="overflow-hidden p-0">
<EditModalContent
teamIdOrSlug={props.teamIdOrSlug}
instance={props.instance}
editEngineInstance={props.editEngineInstance}
closeModal={() => props.onOpenChange(false)}
@@ -372,6 +381,7 @@ const editEngineFormSchema = z.object({
});
function EditModalContent(props: {
teamIdOrSlug: string;
instance: EngineInstance;
editEngineInstance: EditedEngineInstance;
closeModal: () => void;
@@ -396,6 +406,7 @@ function EditModalContent(props: {
onSubmit={form.handleSubmit((data) =>
editInstance.mutate(
{
teamIdOrSlug: props.teamIdOrSlug,
instanceId: props.instance.id,
name: data.name,
url: data.url,
@@ -475,6 +486,7 @@ function EditModalContent(props: {
}
function RemoveModal(props: {
teamIdOrSlug: string;
instance: EngineInstance;
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -491,6 +503,7 @@ function RemoveModal(props: {
(instance.status === "active" && !instance.deploymentId) ? (
<RemoveEngineFromDashboardModalContent
instance={instance}
teamIdOrSlug={props.teamIdOrSlug}
close={() => onOpenChange(false)}
removeEngineFromDashboard={props.removeEngineFromDashboard}
/>
@@ -507,6 +520,7 @@ function RemoveModal(props: {
}
function RemoveEngineFromDashboardModalContent(props: {
teamIdOrSlug: string;
instance: EngineInstance;
close: () => void;
removeEngineFromDashboard: RemovedEngineFromDashboard;
@@ -552,6 +566,7 @@ function RemoveEngineFromDashboardModalContent(props: {
removeFromDashboard.mutate(
{
instanceId: instance.id,
teamIdOrSlug: props.teamIdOrSlug,
},
{
onSuccess: () => {
@@ -12,6 +12,7 @@ export const EngineInstancesList = (props: {
return (
<div className="flex grow flex-col">
<EngineInstancesTable
teamIdOrSlug={props.team_slug}
instances={props.instances}
engineLinkPrefix={engineLinkPrefix}
/>
@@ -29,7 +29,10 @@ export default async function Page(props: {
loginRedirect(`/team/${params.team_slug}/~/engine`);
}
const res = await getEngineInstances({ authToken });
const res = await getEngineInstances({
authToken,
teamIdOrSlug: params.team_slug,
});
return (
<EngineInstancesList
@@ -9,8 +9,12 @@ import { RecentEngineAlertsSection } from "./RecentEngineAlerts";
export function EngineAlertsPage(props: {
instance: EngineInstance;
teamIdOrSlug: string;
}) {
const alertRulesQuery = useEngineAlertRules(props.instance.id);
const alertRulesQuery = useEngineAlertRules(
props.instance.id,
props.teamIdOrSlug,
);
const alertRules = alertRulesQuery.data ?? [];
return (
@@ -19,12 +23,14 @@ export function EngineAlertsPage(props: {
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
teamIdOrSlug={props.teamIdOrSlug}
/>
<div className="h-8" />
<RecentEngineAlertsSection
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
teamIdOrSlug={props.teamIdOrSlug}
/>
</div>
);
@@ -43,19 +43,23 @@ type CreateAlertMutation = UseMutationResult<
>;
export function ManageEngineAlertsSection(props: {
teamIdOrSlug: string;
alertRules: EngineAlertRule[];
alertRulesIsLoading: boolean;
engineId: string;
}) {
const notificationChannelsQuery = useEngineNotificationChannels(
props.engineId,
props.teamIdOrSlug,
);
const deleteAlertMutation = useEngineDeleteNotificationChannel(
props.engineId,
props.teamIdOrSlug,
);
const createAlertMutation = useEngineCreateNotificationChannel(
props.engineId,
props.teamIdOrSlug,
);
// not passing the mutation to avoid multiple rows sharing the same mutation state, we create the new mutation for each row instead in each component instead
@@ -23,10 +23,16 @@ export function RecentEngineAlertsSection(props: {
alertRules: EngineAlertRule[];
alertRulesIsLoading: boolean;
engineId: string;
teamIdOrSlug: string;
}) {
// TODO - pagination
// required : return the total number of alerts in response from API
const alertsQuery = useEngineAlerts(props.engineId, 100, 0);
const alertsQuery = useEngineAlerts(
props.engineId,
props.teamIdOrSlug,
100,
0,
);
const alerts = alertsQuery.data ?? [];
return (
@@ -9,5 +9,7 @@ export default async function Page(props: EngineInstancePageProps) {
teamSlug: params.team_slug,
});
return <EngineAlertsPage instance={instance} />;
return (
<EngineAlertsPage instance={instance} teamIdOrSlug={params.team_slug} />
);
}
@@ -29,7 +29,7 @@ export const EngineConfiguration: React.FC<EngineConfigurationProps> = ({
instanceUrl={instance.url}
authToken={authToken}
/>
<EngineSystem instance={instance} />
<EngineSystem instance={instance} teamIdOrSlug={teamSlug} />
</div>
);
};
@@ -6,11 +6,15 @@ import {
interface EngineSystemProps {
instance: EngineInstance;
teamIdOrSlug: string;
}
export const EngineSystem: React.FC<EngineSystemProps> = ({ instance }) => {
export const EngineSystem: React.FC<EngineSystemProps> = ({
instance,
teamIdOrSlug,
}) => {
const healthQuery = useEngineSystemHealth(instance.url);
const metricsQuery = useEngineSystemMetrics(instance.id);
const metricsQuery = useEngineSystemMetrics(instance.id, teamIdOrSlug);
if (!healthQuery.data) {
return null;
}
@@ -21,9 +25,9 @@ export const EngineSystem: React.FC<EngineSystemProps> = ({ instance }) => {
<br />
Enabled: {healthQuery.data.features?.join(", ")}
<br />
CPU: {metricsQuery.data?.data?.cpu?.toFixed(2) ?? "..."}%
CPU: {metricsQuery.data?.result?.cpu?.toFixed(2) ?? "..."}%
<br />
Memory: {metricsQuery.data?.data?.memory?.toFixed(0) ?? "..."}MB
Memory: {metricsQuery.data?.result?.memory?.toFixed(0) ?? "..."}MB
</p>
);
};
@@ -33,6 +33,7 @@ export default async function Layout(props: {
}
const instance = await getEngineInstance({
teamIdOrSlug: params.team_slug,
authToken,
engineId: params.engineId,
accountId: account.id,
@@ -23,7 +23,7 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
teamSlug,
authToken,
}) => {
const systemMetricsQuery = useEngineSystemMetrics(instance.id);
const systemMetricsQuery = useEngineSystemMetrics(instance.id, teamSlug);
const queueMetricsQuery = useEngineQueueMetrics({
authToken,
instanceUrl: instance.url,
@@ -69,8 +69,10 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
<div className="mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<Healthcheck instance={instance} />
</div>
<StatusCodes datapoints={systemMetricsQuery.data.data.statusCodes} />
<ErrorRate datapoints={systemMetricsQuery.data.data.errorRate} />
<StatusCodes
datapoints={systemMetricsQuery.data.result.statusCodes}
/>
<ErrorRate datapoints={systemMetricsQuery.data.result.errorRate} />
</div>
</Card>
);
@@ -2,6 +2,7 @@ import { API_SERVER_URL } from "@/constants/env";
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
export async function getEngineInstance(params: {
teamIdOrSlug: string;
authToken: string;
engineId: string;
accountId: string;
@@ -19,11 +20,14 @@ export async function getEngineInstance(params: {
return sandboxEngine;
}
const res = await fetch(`${API_SERVER_URL}/v1/engine/${params.engineId}`, {
headers: {
Authorization: `Bearer ${params.authToken}`,
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${params.teamIdOrSlug}/engine/${params.engineId}`,
{
headers: {
Authorization: `Bearer ${params.authToken}`,
},
},
});
);
if (!res.ok) {
const errorMessage = await res.text();
@@ -33,8 +37,8 @@ export async function getEngineInstance(params: {
}
const json = (await res.json()) as {
data: EngineInstance;
result: EngineInstance;
};
return json.data;
return json.result;
}
@@ -20,6 +20,7 @@ export async function engineInstancePageHandler(params: {
}
const instance = await getEngineInstance({
teamIdOrSlug: params.teamSlug,
authToken,
engineId: params.engineId,
accountId: account.id,
@@ -3,12 +3,16 @@ import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
export async function getEngineInstances(params: {
authToken: string;
teamIdOrSlug: string;
}) {
const res = await fetch(`${API_SERVER_URL}/v1/engine`, {
headers: {
Authorization: `Bearer ${params.authToken}`,
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${params.teamIdOrSlug}/engine`,
{
headers: {
Authorization: `Bearer ${params.authToken}`,
},
},
});
);
if (!res.ok) {
const errorMessage = await res.text();
@@ -20,12 +24,10 @@ export async function getEngineInstances(params: {
}
const json = (await res.json()) as {
data: {
instances: EngineInstance[];
};
result: EngineInstance[];
};
return {
data: json.data.instances,
data: json.result,
};
}
@@ -66,7 +66,7 @@ async function ChainDistributionCard({ data }: { data: TransactionStats[] }) {
async function ContractDistributionCard({
data,
}: { data: TransactionStats[] }) {
const reducedData = await Promise.all(
const _reducedData = await Promise.all(
Object.entries(
data
.filter((d) => d.contractAddress)
@@ -83,25 +83,36 @@ async function ContractDistributionCard({
.sort((a, b) => b[1] - a[1])
.slice(0, 10) // only top ten
.map(async ([key, value]) => {
const [chainId, contractAddress] = key.split(":");
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(Number(chainId));
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
const contractData = await fetchDashboardContractMetadata(
getContract({
chain,
address: contractAddress as string, // we filter above
client: getThirdwebClient(),
}),
).catch(() => undefined);
return {
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
link: `/${chainId}/${contractAddress}`,
value,
};
try {
const [chainId, contractAddress] = key.split(":");
if (Number(chainId) === 0) {
return undefined;
}
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(Number(chainId));
const chainMeta = await getChainMetadata(chain).catch(
() => undefined,
);
const contractData = await fetchDashboardContractMetadata(
getContract({
chain,
address: contractAddress as string, // we filter above
client: getThirdwebClient(),
}),
).catch(() => undefined);
return {
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
link: `/${chainId}/${contractAddress}`,
value,
};
} catch {
return undefined;
}
}),
);
const reducedData = _reducedData.filter((d) => !!d);
const aggregateFn = () =>
new Set(
data
@@ -115,19 +115,23 @@ async function AsyncYourFactories(props: {
authToken: props.authToken,
});
const client = getThirdwebClient();
const client = getThirdwebClient(props.authToken);
const factories = (
await Promise.all(
deployedContracts.map(async (c) => {
const contract = getContract({
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(Number(c.chainId)),
address: c.contractAddress,
client,
});
const m = await getCompilerMetadata(contract);
return m.name.indexOf("AccountFactory") > -1 ? c : null;
try {
const contract = getContract({
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(Number(c.chainId)),
address: c.contractAddress,
client,
});
const m = await getCompilerMetadata(contract);
return m.name.indexOf("AccountFactory") > -1 ? c : null;
} catch {
return null;
}
}),
)
).filter((f) => f !== null);
@@ -1,164 +0,0 @@
import { Button } from "@/components/ui/button";
import type { Range } from "components/analytics/date-range-selector";
import { Stat } from "components/analytics/stat";
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
import { AccountAbstractionSummary } from "components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { differenceInDays } from "date-fns";
import { ArrowRightIcon, CableIcon, WalletCardsIcon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import type {
InAppWalletStats,
UserOpStats,
WalletStats,
} from "types/analytics";
import { DateRangeSelector } from "../../../../../../components/analytics/date-range-selector";
import { IntervalSelector } from "../../../../../../components/analytics/interval-selector";
import { DailyConnectionsChartCard } from "./_components/DailyConnectionsChartCard";
import { WalletConnectorsChartCard } from "./_components/WalletConnectorsChartCard";
import { WalletDistributionChartCard } from "./_components/WalletDistributionChartCard";
export function ConnectAnalyticsDashboardUI(props: {
walletUsage: WalletStats[];
aggregateWalletUsage: WalletStats[];
aggregateUserOpUsageQuery?: UserOpStats;
connectLayoutSlug: string;
isPending: boolean;
inAppAggregateQuery?: {
allTimeStats: InAppWalletStats[];
monthlyStats: InAppWalletStats[];
};
setRange: (range: Range) => void;
range: Range;
intervalType: "day" | "week";
setIntervalType: (intervalType: "day" | "week") => void;
}) {
const { totalWallets, uniqueWallets } = useMemo(() => {
return props.aggregateWalletUsage.reduce(
(acc, curr) => {
acc.totalWallets += curr.totalConnections;
acc.uniqueWallets += curr.uniqueWalletsConnected;
return acc;
},
{ uniqueWallets: 0, totalWallets: 0 },
);
}, [props.aggregateWalletUsage]);
return (
<div className="flex flex-col gap-4 lg:gap-6">
<div>
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Connections
</h3>
<p className="text-muted-foreground">
View how many users are connecting to your app.
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 lg:gap-6">
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
<Stat
label="Unique Wallets"
value={uniqueWallets}
icon={WalletCardsIcon}
/>
</div>
</div>
<div className="h-4">
<div className="w-full border-separate border-b" />
</div>
<div>
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
In-App Wallets
</h3>
<p className="text-muted-foreground">
View metrics for your in-app wallets.
</p>
</div>
<div>
<Button asChild variant="primary" size="sm" className="w-full">
<Link href={`${props.connectLayoutSlug}/in-app-wallets`}>
In-App Wallet Metrics
<ArrowRightIcon className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
<InAppWalletsSummary
allTimeStats={props.inAppAggregateQuery?.allTimeStats}
monthlyStats={props.inAppAggregateQuery?.monthlyStats}
/>
</div>
<div className="h-4">
<div className="w-full border-separate border-b" />
</div>
<div>
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Account Abstraction
</h3>
<p className="text-muted-foreground">
View how smart wallets are used in your app.
</p>
</div>
<div>
<Button asChild variant="primary" size="sm" className="w-full">
<Link href={`${props.connectLayoutSlug}/account-abstraction`}>
Account Abstraction Metrics
<ArrowRightIcon className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
<AccountAbstractionSummary
aggregateUserOpUsageQuery={props.aggregateUserOpUsageQuery}
/>
</div>
<div className="h-4">
<div className="w-full border-separate border-b" />
</div>
<div className="flex justify-end gap-3">
<DateRangeSelector
range={props.range}
setRange={(newRange) => {
props.setRange(newRange);
const days = differenceInDays(newRange.to, newRange.from);
props.setIntervalType(days > 30 ? "week" : "day");
}}
/>
<IntervalSelector
intervalType={props.intervalType}
setIntervalType={props.setIntervalType}
/>
</div>
<DailyConnectionsChartCard
walletStats={props.walletUsage}
isPending={props.isPending}
/>
<WalletConnectorsChartCard
walletStats={props.walletUsage}
isPending={props.isPending}
/>
<WalletDistributionChartCard
walletStats={props.walletUsage}
isPending={props.isPending}
/>
</div>
);
}
@@ -1,46 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { createUserOpStatsStub } from "components/smart-wallets/AccountAbstractionAnalytics/storyUtils";
import { getLastNDaysRange } from "../../../../../../../components/analytics/date-range-selector";
import { mobileViewport } from "../../../../../../../stories/utils";
import { ConnectAnalyticsDashboardUI } from "../ConnectAnalyticsDashboardUI";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Analytics Dashboard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container max-w-[1150px] py-8">
<ConnectAnalyticsDashboardUI
setRange={() => {}}
range={getLastNDaysRange("last-120")}
intervalType="day"
setIntervalType={() => {}}
walletUsage={createWalletStatsStub(30)}
aggregateWalletUsage={createWalletStatsStub(30)}
aggregateUserOpUsageQuery={createUserOpStatsStub(1)?.[0]}
connectLayoutSlug="connectLayoutSlug"
isPending={false}
/>
</div>
);
}
@@ -1,70 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { DailyConnectionsChartCard } from "./DailyConnectionsChartCard";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Daily Connections",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container flex max-w-[1150px] flex-col gap-6 py-8">
<BadgeContainer label="30 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(30)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(60)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(10)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isPending={true}
/>
</BadgeContainer>
</div>
);
}
@@ -1,243 +0,0 @@
"use client";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
EmptyChartState,
LoadingChartState,
} from "components/analytics/empty-chart-state";
import { DotNetIcon } from "components/icons/brand-icons/DotNetIcon";
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 { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
import { DocLink } from "components/shared/DocLink";
import { format } from "date-fns";
import { useMemo, useState } from "react";
import {
Bar,
BarChart,
CartesianGrid,
LabelList,
XAxis,
YAxis,
} from "recharts";
import type { WalletStats } from "types/analytics";
import { formatTickerNumber } from "../../../../../../../lib/format-utils";
type ChartToShow = "uniqueWallets" | "totalWallets";
type ChartData = {
time: string; // human readable date
totalWallets: number;
uniqueWallets: number;
};
const chartConfig = {
uniqueWallets: {
label: "Unique Wallets",
color: "hsl(var(--chart-1))",
},
totalWallets: {
label: "Total Wallets",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
const chartLabelToShow: Record<ChartToShow, string> = {
uniqueWallets: "Unique Wallets",
totalWallets: "Total Wallets",
};
export function DailyConnectionsChartCard(props: {
walletStats: WalletStats[];
isPending: boolean;
}) {
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>("uniqueWallets");
const chartToShowOptions: ChartToShow[] = ["uniqueWallets", "totalWallets"];
const barChartData: ChartData[] = useMemo(() => {
const chartDataMap: Map<string, ChartData> = new Map();
for (const data of walletStats) {
const chartData = chartDataMap.get(data.date);
if (!chartData) {
chartDataMap.set(data.date, {
time: format(new Date(data.date), "MMM dd"),
totalWallets: data.totalConnections,
uniqueWallets: data.uniqueWalletsConnected,
});
} else {
chartData.totalWallets += data.totalConnections;
chartData.uniqueWallets += data.uniqueWalletsConnected;
}
}
return Array.from(chartDataMap.values());
}, [walletStats]);
const disableActions = props.isPending || barChartData.length === 0;
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">
Daily Connections
</h3>
<p className="mb-3 text-muted-foreground text-sm">
Total and unique wallets addresses that connected to your app each day.
</p>
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
{/* Selector */}
<Select
onValueChange={(v) => {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={disableActions}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{chartToShowOptions.map((option) => (
<SelectItem key={option} value={option}>
{chartLabelToShow[option]}
</SelectItem>
))}
</SelectContent>
</Select>
<ExportToCSVButton
disabled={disableActions}
className="bg-background"
getData={async () => {
const header = ["Date", "Total Wallets", "Unique Wallets"];
const rows = barChartData.map((row) => [
row.time,
row.totalWallets.toString(),
row.uniqueWallets.toString(),
]);
return { header, rows };
}}
fileName="DailyConnections"
/>
</div>
{/* Chart */}
<ChartContainer
config={chartConfig}
className="h-[250px] w-full md:h-[350px]"
>
{props.isPending ? (
<LoadingChartState />
) : barChartData.length === 0 ? (
<EmptyChartState>
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">
Send your first connect event
</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/getting-started"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react/v5"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react-native/v5"
label="React Native"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/dotnet/getting-started"
label="Unity"
icon={UnityIcon}
/>
<DocLink
link="https://portal.thirdweb.com/unreal-engine/getting-started"
label="Unreal Engine"
icon={UnrealIcon}
/>
<DocLink
link="https://portal.thirdweb.com/dotnet/getting-started"
label=".NET"
icon={DotNetIcon}
/>
</div>
</div>
</EmptyChartState>
) : (
<BarChart
accessibilityLayer
data={barChartData}
margin={{
top: 20,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
tickMargin={10}
axisLine={false}
/>
<YAxis
dataKey={(data) => data[chartToShow]}
tickLine={false}
axisLine={false}
tickFormatter={(value) => formatTickerNumber(value)}
/>
<ChartTooltip
cursor={true}
content={
<ChartTooltipContent
valueFormatter={(value) => formatTickerNumber(Number(value))}
/>
}
/>
<Bar
dataKey={chartToShow}
fill={`var(--color-${chartToShow})`}
radius={8}
strokeWidth={1}
className="stroke-background"
>
{barChartData.length < 50 && (
<LabelList
position="top"
offset={12}
className="invisible fill-foreground sm:visible"
fontSize={12}
formatter={(value: number) => formatTickerNumber(value)}
/>
)}
</Bar>
</BarChart>
)}
</ChartContainer>
</div>
);
}
@@ -1,70 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { WalletConnectorsChartCard } from "./WalletConnectorsChartCard";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Wallet Connectors",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<BadgeContainer label="30 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(30)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(60)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(10)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isPending={true}
/>
</BadgeContainer>
</div>
);
}
@@ -1,260 +0,0 @@
"use client";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
EmptyChartState,
LoadingChartState,
} from "components/analytics/empty-chart-state";
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
import { DocLink } from "components/shared/DocLink";
import { format } from "date-fns";
import { useMemo, useState } from "react";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import type { WalletStats } from "types/analytics";
import {
formatTickerNumber,
formatWalletType,
} from "../../../../../../../lib/format-utils";
type ChartToShow = "uniqueWalletsConnected" | "totalConnections";
type ChartData = Record<string, number> & {
time: string; // human readable date
};
const chartLabelToShow: Record<ChartToShow, string> = {
uniqueWalletsConnected: "Unique Wallets",
totalConnections: "Total Wallets",
};
export function WalletConnectorsChartCard(props: {
walletStats: WalletStats[];
isPending: boolean;
}) {
// show top 10 wallets as distinct, and combine the rest as "Others"
const topWalletsToShow = 10;
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>(
"uniqueWalletsConnected",
);
const chartToShowOptions: ChartToShow[] = [
"uniqueWalletsConnected",
"totalConnections",
];
const { chartConfig, chartData } = useMemo(() => {
const _chartConfig: ChartConfig = {};
const _chartDataMap: Map<string, ChartData> = new Map();
const walletTypeToValueMap: Map<string, number> = new Map();
// for each stat, add it in _chartDataMap
for (const stat of walletStats) {
const chartData = _chartDataMap.get(stat.date);
const { walletType: rawWalletType } = stat;
const walletType = formatWalletType(rawWalletType);
// if no data for current day - create new entry
if (!chartData) {
_chartDataMap.set(stat.date, {
time: format(new Date(stat.date), "MMM dd"),
[walletType]: stat[chartToShow],
} as ChartData);
} else {
chartData[walletType] =
(chartData[walletType] || 0) + stat[chartToShow];
}
walletTypeToValueMap.set(
walletType,
stat[chartToShow] + (walletTypeToValueMap.get(walletType) || 0),
);
}
const walletTypesSorted = Array.from(walletTypeToValueMap.entries())
.sort((a, b) => b[1] - a[1])
.map((w) => w[0]);
const walletTypesToShow = walletTypesSorted.slice(0, topWalletsToShow);
const walletTypesToTagAsOthers = walletTypesSorted.slice(topWalletsToShow);
// replace walletTypesToTagAsOthers walletType with "other"
for (const data of _chartDataMap.values()) {
for (const walletType in data) {
if (walletTypesToTagAsOthers.includes(walletType)) {
data.others = (data.others || 0) + (data[walletType] || 0);
delete data[walletType];
}
}
}
walletTypesToShow.forEach((walletType, i) => {
_chartConfig[walletType] = {
label: walletTypesToShow[i],
color: `hsl(var(--chart-${(i % 10) + 1}))`,
};
});
// Add Other
walletTypesToShow.push("others");
_chartConfig.others = {
label: "Others",
color: "hsl(var(--muted-foreground))",
};
return {
chartData: Array.from(_chartDataMap.values()),
chartConfig: _chartConfig,
};
}, [walletStats, chartToShow]);
const uniqueWalletTypes = Object.keys(chartConfig);
const disableActions = props.isPending || chartData.length === 0;
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">
Connected Wallets
</h3>
<p className="mb-3 text-muted-foreground text-sm">
The different types of wallets used to connect to your app each day.
</p>
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
<Select
onValueChange={(v) => {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={disableActions}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{chartToShowOptions.map((option) => (
<SelectItem key={option} value={option}>
{chartLabelToShow[option]}
</SelectItem>
))}
</SelectContent>
</Select>
<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", ...uniqueWalletTypes];
const rows = chartData.map((data) => {
const { time, ...rest } = data;
return [
time,
...uniqueWalletTypes.map((w) => (rest[w] || 0).toString()),
];
});
return { header, rows };
}}
/>
</div>
{/* Chart */}
<ChartContainer config={chartConfig} className="h-[400px] w-full">
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState>
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">
Connect any wallet to your app
</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/supported-wallets"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React Native"
icon={ReactIcon}
/>
</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 />} />
{uniqueWalletTypes.map((walletType) => {
return (
<Bar
key={walletType}
dataKey={walletType}
fill={chartConfig[walletType]?.color}
radius={4}
stackId="a"
strokeWidth={1}
className="stroke-background"
/>
);
})}
</BarChart>
)}
</ChartContainer>
</div>
);
}
@@ -1,70 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { WalletDistributionChartCard } from "./WalletDistributionChartCard";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Wallet Distribution",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<BadgeContainer label="30 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(30)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(60)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(10)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isPending={true}
/>
</BadgeContainer>
</div>
);
}
@@ -1,263 +0,0 @@
"use client";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
EmptyChartState,
LoadingChartState,
} from "components/analytics/empty-chart-state";
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
import { DocLink } from "components/shared/DocLink";
import { useMemo, useState } from "react";
import { Pie, PieChart } from "recharts";
import type { WalletStats } from "types/analytics";
import {
formatTickerNumber,
formatWalletType,
} from "../../../../../../../lib/format-utils";
type ChartToShow = "totalConnections" | "uniqueWalletsConnected";
type ChartData = {
walletType: string;
value: number;
fill: string;
};
const chartLabelToShow: Record<ChartToShow, string> = {
uniqueWalletsConnected: "Unique Wallets",
totalConnections: "Total Wallets",
};
export function WalletDistributionChartCard(props: {
walletStats: WalletStats[];
isPending: boolean;
}) {
// show these many top wallets as distinct, and combine the rest as "Others"
const topWalletsToShow = 10;
const { walletStats } = props;
const [chartToShow, setChartToShow] =
useState<ChartToShow>("totalConnections");
const chartToShowOptions: ChartToShow[] = [
"totalConnections",
"uniqueWalletsConnected",
];
const { chartConfig, chartData, totalConnections, uniqueConnections } =
useMemo(() => {
const _chartDataMap: Map<string, number> = new Map();
const walletTypeToValueMap: Map<string, number> = new Map();
let _totalConnections = 0;
let _uniqueConnections = 0;
for (const stat of walletStats) {
const { walletType: rawWalletType } = stat;
const walletType = formatWalletType(rawWalletType);
const chartData = _chartDataMap.get(walletType);
_totalConnections += stat.totalConnections;
_uniqueConnections += stat.uniqueWalletsConnected;
_chartDataMap.set(walletType, (chartData || 0) + stat[chartToShow]);
walletTypeToValueMap.set(
walletType,
(walletTypeToValueMap.get(walletType) || 0) + stat[chartToShow],
);
}
const walletTypesSortedByValue = Array.from(
walletTypeToValueMap.entries(),
)
.sort((a, b) => b[1] - a[1])
.map((w) => w[0]);
const walletTypesToShow = walletTypesSortedByValue.slice(
0,
topWalletsToShow,
);
const walletTypesToTagAsOthers =
walletTypesSortedByValue.slice(topWalletsToShow);
for (const walletType of walletTypesToTagAsOthers) {
const val = _chartDataMap.get(walletType);
if (val) {
const othersVal = _chartDataMap.get("others");
_chartDataMap.set("others", othersVal ? othersVal + val : val);
}
_chartDataMap.delete(walletType);
}
const _chartConfig: ChartConfig = {};
walletTypesToShow.forEach((walletType, i) => {
_chartConfig[walletType] = {
label: walletTypesToShow[i],
color: `hsl(var(--chart-${(i % 10) + 1}))`,
};
});
// Add Others
_chartConfig.others = {
label: "Others",
color: "hsl(var(--muted-foreground))",
};
const _chartData: ChartData[] = Array.from(_chartDataMap).map(
([walletType, data]) => {
return {
walletType,
value: data,
fill: _chartConfig[walletType]?.color || "transparent",
};
},
);
// sort the data
_chartData.sort((a, b) => b.value - a.value);
return {
chartData: _chartData,
chartConfig: _chartConfig,
totalConnections: _totalConnections,
uniqueConnections: _uniqueConnections,
};
}, [walletStats, chartToShow]);
const disableActions = props.isPending || chartData.length === 0;
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">
Wallet Distribution
</h3>
<p className="mb-3 text-muted-foreground text-sm">
Distribution of wallet types used to connect to your app.
</p>
{/* Selector */}
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
<Select
onValueChange={(v) => {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={props.isPending || chartData.length === 0}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{chartToShowOptions.map((option) => (
<SelectItem key={option} value={option}>
{chartLabelToShow[option]}
</SelectItem>
))}
</SelectContent>
</Select>
<ExportToCSVButton
fileName="Wallet Distribution"
className="bg-background"
disabled={disableActions}
getData={async () => {
const header = [
"Wallet",
`${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
`Percentage of ${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
];
const rows = chartData.map((d) => {
return [
d.walletType,
d.value.toString(),
`${((d.value / (chartToShow === "totalConnections" ? totalConnections : uniqueConnections)) * 100).toFixed(2)}%`,
];
});
return {
header,
rows,
};
}}
/>
</div>
{/* Chart */}
<ChartContainer
config={chartConfig}
className="mt-6 h-[500px] w-full [&_.recharts-pie-label-text]:fill-foreground"
>
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState>
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">
Connect any wallet to your app
</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/supported-wallets"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React Native"
icon={ReactIcon}
/>
</div>
</div>
</EmptyChartState>
) : (
<PieChart>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
valueFormatter={(v) => {
if (typeof v === "number") {
const sumValue =
chartToShow === "uniqueWalletsConnected"
? uniqueConnections
: totalConnections;
const percentageValue = ((v / sumValue) * 100).toFixed(2);
return `${percentageValue}% - ${formatTickerNumber(v)}`;
}
}}
/>
}
/>
<ChartLegend content={<ChartLegendContent />} className="" />
<Pie
data={chartData}
dataKey="value"
nameKey="walletType"
innerRadius={60}
strokeWidth={2}
stroke="hsl(var(--muted))"
/>
</PieChart>
)}
</ChartContainer>
</div>
);
}
@@ -1,52 +0,0 @@
import type { WalletId } from "thirdweb/wallets";
import type { WalletStats } from "types/analytics";
const walletsToPickFrom: WalletId[] = [
"io.metamask",
"com.binance",
"com.coinbase.wallet",
"io.zerion.wallet",
"me.rainbow",
"com.trustwallet.app",
"com.zengo",
"xyz.argent",
"com.exodus",
"app.phantom",
"com.okex.wallet",
"io.clingon",
"com.broearn",
"com.coinomi",
"com.ripio",
"com.sabay.wallet",
"io.tokoin",
"world.fncy",
"io.copiosa",
];
const pickRandomWallet = () => {
return (
walletsToPickFrom[Math.floor(Math.random() * walletsToPickFrom.length)] ||
"io.metamask"
);
};
export function createWalletStatsStub(days: number): WalletStats[] {
const stubbedData: WalletStats[] = [];
let d = days;
while (d !== 0) {
const uniqueWallets = Math.floor(Math.random() * 100);
stubbedData.push({
date: new Date(2024, 1, d).toLocaleString(),
totalConnections: uniqueWallets + Math.floor(Math.random() * 100),
uniqueWalletsConnected: uniqueWallets,
walletType: pickRandomWallet(),
});
if (Math.random() > 0.7) {
d--;
}
}
return stubbedData;
}
@@ -76,9 +76,8 @@ function Story(props: {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
data: {
secret: new Array(86).fill("x").join(""),
secretHash: new Array(64).fill("x").join(""),
secretMasked: "123...4567",
secret: `sk_${new Array(86).fill("x").join("")}`,
secretMasked: "sk_123...4567",
},
};
}}
@@ -14,7 +14,12 @@ import { Flex, FormControl } from "@chakra-ui/react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { verifyContract } from "app/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/ContractSourcesPage";
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
import { DEFAULT_FEE_BPS, DEFAULT_FEE_RECIPIENT } from "constants/addresses";
import {
DEFAULT_FEE_BPS,
DEFAULT_FEE_BPS_NEW,
DEFAULT_FEE_RECIPIENT,
THIRDWEB_PUBLISHER_ADDRESS,
} from "constants/addresses";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
@@ -161,6 +166,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
const constructorParams =
metadata.abi.find((a) => a.type === "constructor")?.inputs || [];
const defaultFeeRecipientFunction = metadata.abi.find(
(a) => a.type === "function" && a.name === "DEFAULT_FEE_RECIPIENT",
);
const hasInbuiltDefaultFeeConfig =
defaultFeeRecipientFunction &&
metadata.publisher === THIRDWEB_PUBLISHER_ADDRESS;
const [customFactoryNetwork, customFactoryAddress] = Object.entries(
metadata?.factoryDeploymentData?.customFactoryInput
?.customFactoryAddresses || {},
@@ -457,7 +469,9 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
name: params.contractMetadata?.name || "",
contractURI: _contractURI,
defaultAdmin: params.deployParams._defaultAdmin as string,
platformFeeBps: DEFAULT_FEE_BPS,
platformFeeBps: hasInbuiltDefaultFeeConfig
? DEFAULT_FEE_BPS_NEW
: DEFAULT_FEE_BPS,
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
trustedForwarders: params.deployParams._trustedForwarders
? JSON.parse(params.deployParams._trustedForwarders as string)
@@ -473,8 +487,10 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
payees,
shares,
_contractURI,
_platformFeeBps: DEFAULT_FEE_BPS,
_platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
platformFeeBps: hasInbuiltDefaultFeeConfig
? DEFAULT_FEE_BPS_NEW
: DEFAULT_FEE_BPS,
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
};
const salt = params.deployDeterministic
@@ -5,13 +5,13 @@ interface PlatformFeeFieldsetProps {
isMarketplace: boolean;
}
export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = (
props,
) => {
export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = ({
isMarketplace,
}) => {
return (
<Fieldset legend="Platform fees">
<div className="flex flex-col gap-4 md:flex-row">
{props.isMarketplace ? (
{isMarketplace ? (
<p className="mb-3 pt-4 text-muted-foreground text-sm italic">
A 2.5% platform fee is deducted from each sale to support ongoing
platform operations and improvements.{" "}
@@ -285,6 +285,7 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
twAccount,
}) => {
// TODO: clean this up
const functionsWithExtension = useMemo(() => {
const allFunctions = fnsOrEvents.filter((f) => f.type === "function");
const results: ExtensionFunctions[] = [];
@@ -16,7 +16,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { format, fromUnixTime } from "date-fns";
import { TagIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
@@ -24,17 +23,6 @@ import { toast } from "sonner";
import { useActiveAccount } from "thirdweb/react";
import { z } from "zod";
const LazyShareFreeWalletsModal = dynamic(
() =>
import("./share-free-wallets-modal.client").then((mod) => ({
default: mod.ShareFreeWalletsModal,
})),
{
ssr: false,
loading: () => null,
},
);
export type ActiveCouponResponse = {
id: string;
start: number;
@@ -49,10 +37,7 @@ export type ActiveCouponResponse = {
function ApplyCouponCard(props: {
teamId: string | undefined;
onCouponApplied: (
data: ActiveCouponResponse,
isFreeWalletsCoupon: boolean,
) => void;
onCouponApplied: (data: ActiveCouponResponse) => void;
isPaymentSetup: boolean;
}) {
const searchParams = useSearchParams();
@@ -102,9 +87,7 @@ export function ApplyCouponCardUI(props: {
status: number;
data: null | ActiveCouponResponse;
}>;
onCouponApplied:
| ((data: ActiveCouponResponse, isFreeWalletsCoupon: boolean) => void)
| undefined;
onCouponApplied: ((data: ActiveCouponResponse) => void) | undefined;
prefillPromoCode?: string;
scrollIntoView?: boolean;
isPaymentSetup: boolean;
@@ -145,14 +128,7 @@ export function ApplyCouponCardUI(props: {
case 200: {
toast.success("Coupon applied successfully");
if (res.data) {
props.onCouponApplied?.(
res.data,
// prod & dev
values.promoCode === "FREEWALLETS" ||
// new prod code
values.promoCode === "FREEWALLETS24" ||
values.promoCode === "TESTFREEWALLETS",
);
props.onCouponApplied?.(res.data);
}
break;
}
@@ -284,7 +260,6 @@ export function CouponSection(props: {
isPaymentSetup: boolean;
}) {
const address = useActiveAccount()?.address;
const [showShareModal, setShowShareModal] = useState(false);
const [optimisticCouponData, setOptimisticCouponData] = useState<
| {
type: "added";
@@ -358,15 +333,12 @@ export function CouponSection(props: {
<Suspense fallback={<LoadingCouponSection />}>
<ApplyCouponCard
teamId={props.teamId}
onCouponApplied={(coupon, isFreeWalletsCoupon) => {
onCouponApplied={(coupon) => {
setOptimisticCouponData({
type: "added",
data: coupon,
});
if (isFreeWalletsCoupon) {
setShowShareModal(true);
}
activeCoupon.refetch().then(() => {
setOptimisticCouponData(undefined);
});
@@ -375,13 +347,6 @@ export function CouponSection(props: {
/>
</Suspense>
)}
{showShareModal && (
<LazyShareFreeWalletsModal
isOpen={showShareModal}
onOpenChange={setShowShareModal}
/>
)}
</>
);
}
@@ -1,401 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronLeftIcon, MailIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { DynamicHeight } from "../../../../@/components/ui/DynamicHeight";
import { XIcon } from "../../../icons/brand-icons/XIcon";
export function ShareFreeWalletsModal(props: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}) {
const [screen, setScreen] = useState<"base" | "email">("base");
return (
<Dialog open={props.isOpen} onOpenChange={props.onOpenChange}>
<DialogContent className="p-0 md:max-w-[380px]">
<DynamicHeight>
{screen === "base" && (
<div className="fade-in-0 animate-in duration-300">
<DialogHeader className="p-6 text-center">
<div className="mb-4 flex justify-center gap-2">
<VibrantSmileIcon className="size-16" />
</div>
<DialogTitle className="mb-1 text-lg">
Congratulations!
</DialogTitle>
<DialogDescription className="text-balance text-base text-muted-foreground">
You are getting free wallets for the next 12 months
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-3 border-border border-t p-6">
<Button variant="outline" className="gap-2" asChild>
<TrackedLinkTW
href={`https://x.com/intent/post?text=${encodeURIComponent(twitterPostBody)}`}
target="_blank"
category="share-free-wallets"
label="twitter-share"
>
<XIcon className="size-4" />
Share on X
</TrackedLinkTW>
</Button>
<Button
variant="primary"
className="gap-2"
onClick={() => setScreen("email")}
>
<MailIcon className="size-4 shrink-0" />
Share via Email
</Button>
</div>
</div>
)}
{screen === "email" && (
<SendEmailScreen goBack={() => setScreen("base")} />
)}
</DynamicHeight>
</DialogContent>
</Dialog>
);
}
const emailFormSchema = z.object({
email: z.string().email(),
app: z.string(),
});
function SendEmailScreen(props: {
goBack: () => void;
}) {
const form = useForm<z.infer<typeof emailFormSchema>>({
resolver: zodResolver(emailFormSchema),
defaultValues: {
email: "",
app: "default",
},
});
return (
<div className="fade-in-0 animate-in duration-300">
<Form {...form}>
<form onSubmit={form.handleSubmit(() => {})}>
<div className="p-6">
<DialogHeader className="text-center">
<div className="mb-4 flex justify-center gap-2">
<VibrantEmailIcon className="size-16" />
</div>
<DialogTitle className="mb-1 text-lg">
Share via Email
</DialogTitle>
<DialogDescription className="text-balance text-base text-muted-foreground">
Give the gift of unlimited wallets to another dev
</DialogDescription>
</DialogHeader>
<div className="h-4" />
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="[email protected]"
{...field}
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="h-4" />
<FormField
control={form.control}
name="app"
render={({ field }) => (
<FormItem>
<FormLabel>Share with</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => form.setValue("app", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select Email App" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="default">
Default Email App
</SelectItem>
<SelectItem value="gmail">Gmail</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex justify-between gap-3 border-border border-t p-6">
<Button variant="outline" className="gap-2" onClick={props.goBack}>
<ChevronLeftIcon className="size-4" />
Back
</Button>
{!form.formState.isValid ? (
<Button variant="primary" className="gap-2" type="submit">
<MailIcon className="size-4 shrink-0" />
Share via Email
</Button>
) : (
<Button variant="primary" className="gap-2" asChild>
<TrackedLinkTW
href={
form.watch("app") === "gmail"
? openGmailEmailApp(form.watch("email"))
: openDefaultEmailApp(form.watch("email"))
}
target="_blank"
category="share-free-wallets"
label="email-share"
>
<MailIcon className="size-4 shrink-0" />
Share via Email
</TrackedLinkTW>
</Button>
)}
</div>
</form>
</Form>
</div>
);
}
function openDefaultEmailApp(email: string) {
return `mailto:${email}?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
}
function openGmailEmailApp(email: string) {
return `https://mail.google.com/mail/?view=cm&fs=1&to=${email}&su=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
}
const twitterPostBody = `\
Just claimed the free, unlimited wallets offer from @thirdweb! 🚀
No more per-wallet pricing, full support for web, mobile, and console.
Here's to building without constraints ✨
👉 https://thirdweb.com/unlimited-wallets 👈`;
const emailSubject = "You've unlocked Unlimited Wallets!";
const emailPostBody = `\
Hey there,
I just came across something that might interest you. thirdweb is offering free, unlimited wallets for developers.
I've claimed it and thought you might want to take a look too.
Here's the lowdown:
* It's completely free
* No more per-wallet pricing constraints
* Full support for web, mobile, and console development
I'm pretty excited about the possibilities this opens up for building without limitations. You can check it out here: https://thirdweb.com/unlimited-wallets
Let me know what you think if you decide to give it a try!`;
function VibrantSmileIcon(props: {
className?: string;
}) {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
fill="url(#paint0_linear_754_5630)"
fillOpacity="0.08"
/>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
stroke="url(#paint1_linear_754_5630)"
strokeWidth="2"
/>
<path
d="M26.2857 34.8567C26.2857 34.8567 28.4286 37.7139 32 37.7139C35.5714 37.7139 37.7143 34.8567 37.7143 34.8567M36.2857 27.7139H36.3M27.7143 27.7139H27.7286M46.2857 31.9996C46.2857 39.8894 39.8898 46.2853 32 46.2853C24.1102 46.2853 17.7143 39.8894 17.7143 31.9996C17.7143 24.1098 24.1102 17.7139 32 17.7139C39.8898 17.7139 46.2857 24.1098 46.2857 31.9996ZM37 27.7139C37 28.1084 36.6802 28.4282 36.2857 28.4282C35.8912 28.4282 35.5714 28.1084 35.5714 27.7139C35.5714 27.3194 35.8912 26.9996 36.2857 26.9996C36.6802 26.9996 37 27.3194 37 27.7139ZM28.4286 27.7139C28.4286 28.1084 28.1088 28.4282 27.7143 28.4282C27.3198 28.4282 27 28.1084 27 27.7139C27 27.3194 27.3198 26.9996 27.7143 26.9996C28.1088 26.9996 28.4286 27.3194 28.4286 27.7139Z"
stroke="url(#paint2_linear_754_5630)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_754_5630"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint1_linear_754_5630"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint2_linear_754_5630"
x1="15.7735"
y1="26.8818"
x2="44.9647"
y2="47.5678"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
</defs>
</svg>
);
}
function VibrantEmailIcon(props: {
className?: string;
}) {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
fill="url(#paint0_linear_754_9605)"
fillOpacity="0.08"
/>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
stroke="url(#paint1_linear_754_9605)"
strokeWidth="2"
/>
<path
d="M17.7144 24.857L29.3785 33.0219C30.3231 33.6831 30.7953 34.0137 31.309 34.1417C31.7628 34.2548 32.2374 34.2548 32.6911 34.1417C33.2048 34.0137 33.6771 33.6831 34.6216 33.0219L46.2858 24.857M24.5715 43.4284H39.4286C41.8289 43.4284 43.029 43.4284 43.9457 42.9613C44.7522 42.5504 45.4078 41.8948 45.8187 41.0884C46.2858 40.1716 46.2858 38.9715 46.2858 36.5713V27.4284C46.2858 25.0282 46.2858 23.8281 45.8187 22.9113C45.4078 22.1049 44.7522 21.4493 43.9457 21.0384C43.029 20.5713 41.8289 20.5713 39.4286 20.5713H24.5715C22.1713 20.5713 20.9712 20.5713 20.0544 21.0384C19.248 21.4493 18.5924 22.1049 18.1815 22.9113C17.7144 23.8281 17.7144 25.0282 17.7144 27.4284V36.5713C17.7144 38.9715 17.7144 40.1716 18.1815 41.0884C18.5924 41.8948 19.248 42.5504 20.0544 42.9613C20.9712 43.4284 22.1713 43.4284 24.5715 43.4284Z"
stroke="url(#paint2_linear_754_9605)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_754_9605"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint1_linear_754_9605"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint2_linear_754_9605"
x1="15.7736"
y1="27.9056"
x2="40.3444"
y2="49.6705"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
</defs>
</svg>
);
}
@@ -1,37 +0,0 @@
import { Button } from "@/components/ui/button";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { mobileViewport } from "../../../../stories/utils";
import { ShareFreeWalletsModal } from "./share-free-wallets-modal.client";
const meta = {
title: "Billing/Coupons/ShareFreeWalletsModal",
component: Variants,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Variants>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Variants() {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="flex grow items-center justify-center p-6">
<Button onClick={() => setIsOpen(true)}>Open Again</Button>
<ShareFreeWalletsModal isOpen={isOpen} onOpenChange={setIsOpen} />
</div>
);
}
@@ -44,11 +44,10 @@ export const SmartWalletsBillingAlert = ({
<AlertTitle>Account Abstraction on Mainnet</AlertTitle>
<AlertDescription>
<Text as="span" pr={1}>
You&apos;ve enabled Account Abstraction for one of your API keys
and haven&apos;t added a payment method.
You&apos;ve enabled Account Abstraction for one of your API keys.
<br />
To use them on Mainnet,
</Text>
To enable AA on mainnet chains,
</Text>{" "}
<TrackedLink
href="/team/~/~/settings/billing"
category="api_keys"
@@ -57,7 +56,7 @@ export const SmartWalletsBillingAlert = ({
color="blue.500"
>
<Text as="span" color="blue.500">
add a payment method.
subscribe to a billing plan.
</Text>
</TrackedLink>
</AlertDescription>
@@ -1,7 +1,11 @@
export const THIRDWEB_DEPLOYER_ADDRESS =
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
export const THIRDWEB_PUBLISHER_ADDRESS =
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
export const DEFAULT_FEE_RECIPIENT =
"0x1af20c6b23373350ad464700b5965ce4b0d2ad94";
export const DEFAULT_FEE_BPS = 250;
export const DEFAULT_FEE_BPS_NEW = 150;
-11
View File
@@ -5,14 +5,3 @@ const compactNumberFormatter = new Intl.NumberFormat("en-US", {
export const formatTickerNumber = (value: number) => {
return compactNumberFormatter.format(value);
};
export const formatWalletType = (walletType: string) => {
return walletType.toLowerCase().includes("inapp") ||
walletType.toLowerCase().includes("embedded")
? "in-app"
: walletType.toLowerCase().includes("metamask")
? "metamask"
: walletType.toLowerCase().includes("coinbase")
? "coinbase"
: walletType.toLowerCase();
};
+5 -5
View File
@@ -23,8 +23,8 @@
"@radix-ui/react-tooltip": "1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jose": "6.0.7",
"lucide-react": "0.476.0",
"jose": "6.0.8",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-themes": "^0.4.4",
"react": "19.0.0",
@@ -37,13 +37,13 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.0",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint": "^9",
"eslint": "9.21.0",
"eslint-config-next": "15.2.0",
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
+6 -6
View File
@@ -31,12 +31,12 @@
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "1.1.8",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"@thirdweb-dev/engine": "^0.0.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jose": "6.0.7",
"lucide-react": "0.476.0",
"jose": "6.0.8",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-themes": "^0.4.4",
"nextjs-toploader": "^1.6.12",
@@ -55,15 +55,15 @@
"zod": "3.24.2"
},
"devDependencies": {
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint": "8.57.0",
"eslint-config-next": "15.2.0",
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
@@ -113,8 +113,8 @@ function NoFundsPopup() {
<CodeExample
preview={<PayTransactionButtonPreview />}
code={`import { trasnfer } from "thirdweb/extensions/erc1155";
import { PayEmbed, useActiveAccount } from "thirdweb/react";
code={`import { transfer } from "thirdweb/extensions/erc1155";
import { TransactionButton, useActiveAccount } from "thirdweb/react";
function App() {
@@ -157,13 +157,7 @@ export function BlueprintPlaygroundUI(props: {
const defaultValues = useMemo(() => {
const values: Record<string, string | number> = {};
for (const param of parameters) {
if (param.name === "chain") {
values.chain = "1";
} else if (
param.schema &&
"type" in param.schema &&
param.schema.default
) {
if (param.schema && "type" in param.schema && param.schema.default) {
values[param.name] = param.schema.default;
} else {
values[param.name] = "";
@@ -3,24 +3,21 @@ import type { ConnectButtonProps } from "thirdweb/react";
import { ecosystemWallet } from "thirdweb/wallets";
import { StyledConnectEmbed } from "../styled-connect-embed";
const getEcosystem = () => {
const getEcosystemWallet = () => {
if (
process.env.NEXT_PUBLIC_IN_APP_WALLET_URL?.endsWith(".thirdweb-dev.com")
) {
// dev ecosystem
return "ecosystem.catlovers";
return ecosystemWallet("ecosystem.catlovers");
}
// prod ecosystem
return "ecosystem.thirdweb-engs";
return ecosystemWallet("ecosystem.thirdweb-engs", {
partnerId: "50fd8850-3c6c-48dd-969c-622f88b52d95",
});
};
export function EcosystemConnectEmbed(
props?: Omit<ConnectButtonProps, "client" | "theme">,
) {
return (
<StyledConnectEmbed
{...props}
wallets={[ecosystemWallet(getEcosystem())]}
/>
);
return <StyledConnectEmbed {...props} wallets={[getEcosystemWallet()]} />;
}
@@ -35,7 +35,7 @@ export function PayTransactionPreview() {
const { theme } = useTheme();
const { data: nft } = useReadContract(getNFT, {
contract: nftContract,
tokenId: 1n,
tokenId: 2n,
});
return (
@@ -51,7 +51,7 @@ export function PayTransactionPreview() {
transaction: claimTo({
contract: nftContract,
quantity: 1n,
tokenId: 1n,
tokenId: 2n,
to: account?.address || "",
}),
metadata: nft?.metadata,
+5 -5
View File
@@ -27,7 +27,7 @@
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"@tryghost/content-api": "^1.11.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -35,7 +35,7 @@
"flexsearch": "^0.7.43",
"github-slugger": "^2.0.0",
"he": "^1.2.0",
"lucide-react": "0.476.0",
"lucide-react": "0.477.0",
"next": "15.2.0",
"nextjs-toploader": "^1.6.12",
"node-html-markdown": "^1.3.0",
@@ -59,10 +59,10 @@
"@types/flexsearch": "^0.7.6",
"@types/he": "^1.2.3",
"@types/mdx": "^2.0.13",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/react-html-parser": "^2.0.6",
"@types/react-html-parser": "^2.0.7",
"@types/tryghost__content-api": "^1.3.16",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
@@ -77,6 +77,6 @@
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"tsx": "4.19.3",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
@@ -27,6 +27,42 @@ With a thirdweb API key, you get access to bundler and paymaster infrastructure
To support a chain not listed, [contact us](https://thirdweb.com/contact-us).
## Using Paymaster & Bundler URL
To use the Paymaster & Bundler URL for thirdweb, you can follow the guide below:
### URL Pattern
The URL pattern is predictable for all chains and is used for both paymaster and bundler operations:
```
https://<chain_id>.bundler.thirdweb.com/v2/<thirdweb-client-id>
```
Replace `<chain_id>` with the appropriate chain ID and `<thirdweb-client-id>` with your thirdweb client ID.
### Client ID in Header
You can also pass the client ID via the header:
```
x-client-id: <your_thirdweb_client_id>
```
### Frontend Calls
For frontend calls, the calling domain needs to be allowlisted. This can be done from the thirdweb dashboard in the API key settings.
### Backend Usage
For backend usage, the secret key is required as a header, and the client ID can be omitted:
```
x-secret-key: <your_thirdweb_secret_key>
```
Make sure to replace the placeholders with your actual values. This guide will help you set up and use Paymaster & Bundler URLs effectively with thirdweb.
## Using a Custom Bundler
Smart accounts are free to use with your own account abstraction infrastructure. To use your own paymaster & bundler,
@@ -1,17 +1,17 @@
import { createMetadata, DocImage } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Customize ConnectButton",
title: "thirdweb Universal Bridge - Customize ConnectButton",
icon: "thirdweb",
},
title: "thirdweb Pay - Customize ConnectButton - thirdweb",
title: "thirdweb Universal Bridge - Customize ConnectButton - thirdweb",
description:
"thirdweb Pay - Customizing ConnectButton onramps and crypto purchase flows",
"thirdweb Universal Bridge - Customizing ConnectButton onramps and crypto purchase flows",
});
# Customize ConnectButton
Learn how to customize Pay within the `ConnectButton` interface. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/ConnectButtonProps).
Learn how to customize Universal Bridge within the `ConnectButton` interface. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/ConnectButtonProps).
---
@@ -39,7 +39,7 @@ You can enable users to select your token on a given chain by passing an array o
## Prefill Destination Token
In the case you want your users to purchase your token by default in your application, you can choose to pre-fill the Pay purchase flow with `prefillBuy` .
In the case you want your users to purchase your token by default in your application, you can choose to pre-fill the Universal Bridge purchase flow with `prefillBuy` .
For example, if you wanted users to only purchase Ethereum on Base network, you could do as follows:
@@ -137,7 +137,7 @@ In some cases, you may only want to show users fiat or crypto payment options fo
## Theme
You can set the theme for the Pay component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
You can set the theme for the Universal Bridge component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
We have [`lightTheme`](/references/typescript/v5/lightTheme) or [`darkTheme`](/references/typescript/v5/darkTheme) providers that you can override to kickstart customization.
@@ -1,12 +1,12 @@
import { createMetadata, DocImage } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Customize PayEmbed",
title: "thirdweb Universal Bridge - Customize PayEmbed",
icon: "thirdweb",
},
title: "thirdweb Pay - Customize PayEmbed - thirdweb",
title: "thirdweb Universal Bridge - Customize PayEmbed - thirdweb",
description:
"thirdweb Pay - Customizing embeddable onramps and crypto purchase flows",
"thirdweb Universal Bridge - Customizing embeddable onramps and crypto purchase flows",
});
# PayEmbed Customization
@@ -129,7 +129,7 @@ In some cases, you may only want to show users fiat or crypto payment options fo
## Theme
You can set the theme for the Pay component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
You can set the theme for the component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
We have [`lightTheme`](/references/typescript/v5/lightTheme) or [`darkTheme`](/references/typescript/v5/darkTheme) providers that you can override to kickstart customization.
@@ -1,17 +1,17 @@
import { createMetadata, DocImage } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Customize sendTransaction",
title: "thirdweb Universal Bridge - Customize sendTransaction",
icon: "thirdweb",
},
title: "thirdweb Pay - Customize sendTransaction - thirdweb",
title: "thirdweb Universal Bridge - Customize sendTransaction - thirdweb",
description:
"thirdweb Pay - Customizing point of sale onramps and crypto purchase flows",
"thirdweb Universal Bridge- Customizing point of sale onramps and crypto purchase flows",
});
# useSendTransaction Customization
Learn how to customize the Pay interface when executing a transaction with `useSendTransaction`. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/SendTransactionConfig).
Learn how to customize the Universal Bridge interface when executing a transaction with `useSendTransaction`. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/SendTransactionConfig).
---
@@ -123,7 +123,7 @@ const { mutate: sendTransaction } = useSendTransaction({
## Theme
You can set the theme for the Pay component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
You can set the theme for the component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
We have [`lightTheme`](/references/typescript/v5/lightTheme) or [`darkTheme`](/references/typescript/v5/darkTheme) providers that you can override to kickstart customization.
+29 -27
View File
@@ -1,40 +1,39 @@
import { createMetadata } from "@doc";
import { createMetadata, Details } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - FAQs",
title: "thirdweb Universal Bridge - FAQs",
icon: "thirdweb",
},
title: "thirdweb Pay - FAQs - thirdweb",
description: "thirdweb Pay - Frequently Asked Questions",
title: "thirdweb Universal Bridge - FAQs - thirdweb",
description: "thirdweb Universal Bridge - Frequently Asked Questions",
});
# FAQs
### What are the fees for Buy With Fiat?
<Details summary="What are the fees for Buy With Fiat?">
thirdweb takes a 1% fee on all Buy With Fiat transactions. Additional processing fees will depend on the selected onramp vendor.
</Details>
### What are the fees for Buy With Crypto?
<Details summary="What are the fees for Buy With Crypto?">
thirdweb takes a 1% fee on all Buy With Crypto transactions.
</Details>
### How does fee sharing work for Buy With Crypto?
<Details summary="How does fee sharing work for Buy With Crypto?">
thirdweb shares 70% of all fees collected through Buy With Crypto with our developers. Fee sharing is not currently available for Buy With Fiat transactions.
</Details>
### What does the term “network fees” refer to?
<Details summary="What does the term “network fees” refer to?">
Network fees, also referred to as miner fees, are paid to the miner for processing crypto transactions and securing the respective network. These fees do not go to thirdweb.
</Details>
### What are your transaction limits for Buy With Fiat?
<Details summary="What are your transaction limits for Buy With Fiat?">
We currently require a $1 minimum purchase for both stablecoin and non-stablecoin purchases through Buy With Fiat.
Our transaction maximum starts from $1500 per week for new users and can increase over time to $10,000 per week.
</Details>
### What countries does Buy With Fiat support?
<Details summary="What countries does Buy With Fiat support?">
**Buy With Fiat** is available 130+ countries. The following countries are _UNSUPPORTED_:
- Afghanistan
@@ -60,9 +59,9 @@ Our transaction maximum starts from $1500 per week for new users and can increas
- Yemen
All United States are supported, excluding Hawaii.
</Details>
### Does thirdweb offer direct onramping to tokens through Buy With Fiat?
<Details summary="Does thirdweb offer direct onramping to tokens through Buy With Fiat?">
We offer direct onramping **internationally** to the following tokens:
- Mainnet (Ethereum)
@@ -80,19 +79,22 @@ We offer direct onramping to the following tokens in the **US only**:
All other tokens will require an additional Buy With Crypto step.
\* Not available in New York or the EU.
</Details>
### How does KYC work with Buy With Fiat
<Details summary="How does KYC work with Buy With Fiat">
Users will be required to complete KYC verification for their first purchase using 'Buy with Fiat.' After that, no further KYC will be needed for future purchases on the same platform.
</Details>
### What cryptocurrencies does Pay support?
Pay can support any currency on our [supported chains](/connect/pay/supported-chains).
### How are cryptocurrency prices set in thirdweb Pay?
<Details summary="What cryptocurrencies does Universal Bridge support?">
Universal Bridge can support any currency on our [supported chains](/connect/pay/supported-chains).
</Details>
<Details summary="How are cryptocurrency prices set in thirdweb Universal Bridge?">
Prices are set depending on the liquidity provider used to route your transaction.
</Details>
### Does thirdweb Pay offer fiat payouts?
<Details summary="Does thirdweb Universal Bridge offer fiat payouts?">
Fiat payouts are not currently available.
</Details>
@@ -3,11 +3,11 @@ import paySettingsImage from "../assets/pay-settings.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Fee Sharing",
title: "thirdweb Universal Bridge - Fee Sharing",
icon: "thirdweb",
},
title: "thirdweb Pay - Fee Sharing | thirdweb",
description: "How to participate in fee sharing with thirdweb Pay.",
title: "thirdweb Universal Bridge - Fee Sharing | thirdweb",
description: "How to participate in fee sharing with thirdweb Universal Bridge",
});
# Fee Sharing
@@ -18,6 +18,6 @@ For example, if a user purchases $100 worth of Polygon through your application,
Plase note that fee sharing only applies to fees collected through swaps and bridges. Fiat purchases do not currently qualify for fee sharing.
To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Pay in [thirdweb dashboard](https://www.thirdweb.com/team).
To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Universal Bridge in [thirdweb dashboard](https://www.thirdweb.com/team).
<DocImage src={paySettingsImage} />
@@ -10,19 +10,19 @@ import GetStartedSend from "../assets/get-started-send.png";
import GetStartedEmbed from "../assets/get-started-embed.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Get Started",
title: "thirdweb Universal Bridge - Get Started",
icon: "thirdweb",
},
title: "thirdweb Pay - Buy With Fiat - thirdweb",
title: "Universal Bridge Implementation Guide — thirdweb docs",
description:
"Get Started with Pay - Onchain Purchases Directly From Credit or Debit Card",
"Learn how to implement all-in-one web3 payment solution, Universal bridge: the technical step-by-step process",
});
# Get Started
Learn how to add onramps and crypto purchases to your application. To see which integration is right for you, refer to our [integrations overview](/connect/pay/overview#integration-options).
The following guide uses our React SDK. You can also learn how to integrate Pay with our [Unity SDK](https://portal.thirdweb.com/unity/pay/getbuywithcryptoquote).
The following guide uses our React SDK. You can also learn how to integrate Universal Bridge with our [Unity SDK](https://portal.thirdweb.com/unity/pay/getbuywithcryptoquote).
---
@@ -66,7 +66,7 @@ Our `ConnectButton` has a number of [customization options](/connect/pay/customi
---
## Option 2: Embed Pay
## Option 2: Embed
The `PayEmbed` allows users to onramp and purchase crypto directly from your application interface.
@@ -84,7 +84,7 @@ export default function App() {
}
```
The embedded Pay component will be displayed like so in your application interface:
The embedded component will be displayed like so in your application interface:
<DocImage src={GetStartedEmbed} />
@@ -96,7 +96,7 @@ Our `PayEmbed` has a number of [customization options](/connect/pay/customizatio
## Option 3: Send a Transaction with Pay
Pay is enabled by default for any contract call sent with `sendTransaction`. It will automatically be invoked when a user does not have enough funds to complete the transaction.
Universal Bridge is enabled by default for any contract call sent with `sendTransaction`. It will automatically be invoked when a user does not have enough funds to complete the transaction.
<Callout variant="info">
ℹ️ `sendTransaction` is available in [Connect SDK v5](/typescript/v5) or
@@ -123,10 +123,10 @@ const onClick = () => {
};
```
When a user clicks this button, Pay will perform gas estimation to determine if the user has a sufficient balance. If their balance is sufficient, the transaction will execute normally. If their balance is not sufficient, the following modal will pop up asking them to either onramp funds or convert crypto to the required amount:
When a user clicks this button, Universal Bridge will perform gas estimation to determine if the user has a sufficient balance. If their balance is sufficient, the transaction will execute normally. If their balance is not sufficient, the following modal will pop up asking them to either onramp funds or convert crypto to the required amount:
<DocImage src={GetStartedSend} />
Once a user onramps or converts their funds to the required token, the user will be prompted once more to confirm the transaction. The transaction will then execute as expected.
For deeper customization of the Pay transaction modal, you can refer to our [sendTransaction customization guide](/connect/pay/customization/send-transaction).
For deeper customization of the Universal Bridge transaction modal, you can refer to our [sendTransaction customization guide](/connect/pay/customization/send-transaction).
@@ -3,17 +3,17 @@ import DirectPaymentsFlow from "../../assets/direct-payments-flow.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Accept Direct Payments",
title: "thirdweb Universal Bridge - Accept Direct Payments",
icon: "thirdweb",
},
title: "thirdweb Pay - Accept Direct Payments | thirdweb",
title: "thirdweb Universal Bridge - Accept Direct Payments | thirdweb",
description:
"Combine Pay and Engine to create a smooth point of sale experience on any chain",
"Combine Universal Bridge and Engine to create a smooth point of sale experience on any chain",
});
# Accept Direct Payments with Pay & Engine
# Accept Direct Payments with Universal Bridge & Engine
Learn how to accept fiat and crypto from your customers with Pay and trigger Engine actions, like minting NFTs or transferring tokens to user wallets.
Learn how to accept fiat and crypto from your customers with Universal Bridge and trigger Engine actions, like minting NFTs or transferring tokens to user wallets.
### Requirements
@@ -26,11 +26,11 @@ Before jumping in, it's important to understand the full system, from initial pu
<DocImage src={DirectPaymentsFlow} />
1. **User Initiates Purchase:** User sends specified funds (ETH, MATIC, USD, etc.) to your wallet via thirdweb Pay.
1. **User Initiates Purchase:** User sends specified funds (ETH, MATIC, USD, etc.) to your wallet via thirdweb Universal Bridge.
- Your wallet can be any business wallet you choose or your Engine backend wallet.
- Use the `purchaseData` field to pass any item data needed in the webhook.
E.g., `{ nftId: 1 }`.
2. **Pay Processes Purchase:** thirdweb Pay processes the transaction and sends a [Purchase Complete webhook](/connect/pay/webhooks#purchase-complete) to your dapp’s server when the transaction is complete.
2. **Universal Bridge Processes Purchase:** thirdweb Universal Bridge processes the transaction and sends a [Purchase Complete webhook](/connect/pay/webhooks#purchase-complete) to your dapp’s server when the transaction is complete.
3. **Your Server Calls Engine:** Your server processes the webhook and sends a contract call to the thirdweb Engine backend wallet to perform the item purchase.
4. **Engine Executes the Transaction:** Engine backend wallet executes the transaction needed to deliver the token(s) to the user’s address.
5. **(Optional) User Is Notified :** Your server confirms the completed Engine transaction and notifies the user.
@@ -52,11 +52,11 @@ Now, let's get started.
4. Make a plan to monitor and keep the float wallet topped up to avoid any issues with the service.
</Step>
<Step title="Integrate thirdweb Pay (Client)">
1. [Integrate thirdweb Pay](https://portal.thirdweb.com/connect/pay/build-a-custom-experience) on the client side to enable fiat and crypto payments.
<Step title="Integrate thirdweb Universal Bridge (Client)">
1. [Integrate thirdweb Universal Bridge](https://portal.thirdweb.com/connect/pay/build-a-custom-experience) on the client side to enable fiat and crypto payments.
2. Specify the required parameters for your Pay transaction.
2. Specify the required parameters for your Universal Bridge transaction.
```ts
const quote = await getBuyWithFiatQuote({
@@ -84,9 +84,9 @@ Now, let's get started.
|purchaseData|`Object`|Any details you need to pass in order to track your transaction. If this is a secondary sale, you may want to include details like `{ nftId: 1, orderId: 1 }` |
</Step>
<Step title="Integrate thirdweb Pay (Server)">
1. Log in & create a “Purchase Complete” webhook in your Team > Connect > Pay > Webhooks page.
2. The [Pay webhook POST response](/connect/pay/webhooks#purchase-complete) will include the BuyWithCryptoStatus or BuyWithFiatStatus in the body.
<Step title="Integrate thirdweb Universal Bridge (Server)">
1. Log in & create a “Purchase Complete” webhook in your Team > Connect > Universal Bridge > Webhooks page.
2. The [Universal Bridge webhook POST response](/connect/pay/webhooks#purchase-complete) will include the BuyWithCryptoStatus or BuyWithFiatStatus in the body.
* This will also include the purchaseData you previously passed (metadata identifying the customer information, etc.).
* A request will be sent on every status change.
3. Listen for [completed purchase statuses](https://portal.thirdweb.com/connect/pay/build-a-custom-experience#Poll%20for%20Transaction%20Status). Note that if this is a two-step BuyWithFiat transaction (onramp, then swap), you should also listen for `CRYPTO_SWAP_COMPLETED`
@@ -10,12 +10,12 @@ import OnrampStepOne from "../../assets/avax-to-usd.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Custom Experience",
title: "thirdweb Universal Bridge - Custom Experience",
icon: "thirdweb",
},
title: "thirdweb Pay - Buy With Fiat - Custom Experience | thirdweb",
title: "thirdweb Universal Bridge - Buy With Fiat - Custom Experience | thirdweb",
description:
"Learn how to build a custom onramp experience with thirdweb Pay.",
"Learn how to build a custom onramp experience with thirdweb Universal Bridge.",
});
# Build a Custom Fiat Experience
@@ -2,17 +2,17 @@ import { createMetadata } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Onramp Providers",
title: "thirdweb Universal Bridge - Onramp Providers",
icon: "thirdweb",
},
title: "thirdweb Pay - Onramp Providers | thirdweb",
title: "thirdweb Universal Bridge - Onramp Providers | thirdweb",
description:
"Integrate onramp providers for any onchain transaction and set preferred providers",
});
# Onramp Providers
thirdweb Pay integrates with Stripe, Kado and Transak to power our onramp. By default, we choose a recommended provider based on the location of the user, KYC status, and currency.
thirdweb Universal Bridge integrates with Stripe, Kado and Transak to power our onramp. By default, we choose a recommended provider based on the location of the user, KYC status, and currency.
| Provider | Supported Countries |
| -------- | ------------------------------------------------------------------------------------------------------- |
@@ -1,19 +1,20 @@
import { createMetadata, DocImage, Grid, SDKCard, FeatureCard } from "@doc";
import PayOverviewImage from "../assets/pay-overview.png";
import SupportedChains from "../../../_images/supported-chains.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay",
title: "thirdweb Universal Bridge",
icon: "thirdweb",
},
title: "thirdweb Pay - thirdweb",
title: "thirdweb Universal Bridge Docs: Web3 Payments, On-ramping, bridging & swapping",
description:
"thirdweb Pay - Fast, flexible credit card and crypto point of sale",
"Learn everything about thirdweb’s web3 payments solution, Universal Bridge. Technical docs on onramping, bridging + swapping.",
});
# Overview
Pay allows your users to purchase cryptocurrencies and execute transactions with their credit card or debit card, or with any token via cross-chain routing.
Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencies and execute transactions with any fiat options or tokens via cross-chain routing.
<DocImage src={PayOverviewImage} />
@@ -67,14 +68,30 @@ Pay allows your users to purchase cryptocurrencies and execute transactions with
</div>
### Use Cases
## Supported Chains
- **Onramp funds directly into any transaction,** perfect for any of the following use cases:
- **Games** selling in-game assets onchain.
- **DEXes** looking to help users deposit funds.
- **Staking apps** onboarding new users to their pools.
- **Social apps** looking to add tipping or other paid functionality to their application.
- **Non-profits** accepting donations in crypto.
Universal Bridge is supported on select EVM compatible chains. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist?service=pay).
<DocImage src={SupportedChains} />
## Fee Sharing
Thirdweb collects a 1% fee per end-user transaction and shares 70% of that fee with you. For example, on a $100 Polygon purchase, a $1.00 fee is charged—of which $0.30 is retained by thirdweb and $0.70 is remitted to you.
Note: Fee sharing applies only to fees from swaps and bridges; fiat purchases are excluded.
To enable fee sharing, simply set a recipient address in your dashboard under Team > Project > Connect > Universal Bridge in the thirdweb dashboard.
## Onramp providers
| Provider | Supported Countries |
| -------- | ------------------------------------------------------------------------------------------------------- |
| Transak | https://transak.notion.site/On-Ramp-Payment-Methods-Fees-Other-Details-b0761634feed4b338a69f4f186d906a5 |
| Kado | https://www.kado.money/supported-countries |
| Stripe | https://docs.stripe.com/crypto/onramp |
Specify a preferred onramp provider by setting the preferredProvider parameter in ConnectButton, PayEmbed, or Headless Integration. For details, refer to
[customization](../pay/customization/connectbutton#preferredProvider) or the [API Reference for preferredProvider](/references/typescript/v5/GetBuyWithFiatQuoteParams).
## Integration Options
@@ -83,4 +100,4 @@ Pay allows your users to purchase cryptocurrencies and execute transactions with
| [ConnectButton](/connect/pay/get-started#option-1-connectbutton) | Developers who want out-of-the-box support complete with wallet and social login. |
| [PayEmbed](/connect/pay/get-started#option-2-embed-pay) | Developers who want onramp and crypto purchase experiences directly in their application. |
| [Transaction Flow (sendTransaction)](/connect/pay/get-started#option-3-send-a-transaction-with-pay) | Developers who want users to onramp or purchase crypto directly into a transaction. Great for minting and NFT purchase flows. |
| [Custom Pay Experience](/connect/pay/build-a-custom-experience) | Developers who want full customization over every step of the onramp process, from displaying quotes to polling for transaction statuses. |
| [Custom Universal Bridge Experience](/connect/pay/build-a-custom-experience) | Developers who want full customization over every step of the onramp process, from displaying quotes to polling for transaction statuses. |
@@ -2,15 +2,15 @@ import { createMetadata } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Supported Chains",
title: "thirdweb Universal Bridge - Supported Chains",
icon: "thirdweb",
},
title: "thirdweb Pay - Supported Chains | thirdweb",
description: "List of chains supported by Pay.",
title: "thirdweb Universal Bridge - Supported Chains | thirdweb",
description: "List of chains supported by Universal Bridge.",
});
# Supported Chains
[View all supported chains with Pay](https://thirdweb.com/chainlist?service=pay).
[View all supported chains with Universal Bridge](https://thirdweb.com/chainlist?service=pay).
If you’d like to suggest a chain for us to support, [contact us](https://thirdweb.com/contact-us).
@@ -6,8 +6,8 @@ export const metadata = createMetadata({
title: "thirdweb Pay - Test Mode",
icon: "thirdweb",
},
title: "thirdweb Pay - Test Mode - thirdweb",
description: "thirdweb Pay - Test Mode",
title: "Enable Test Mode for Universal bridge — thirdweb docs",
description: "Learn how to enable test mode for web3 payments with the thirdweb Universal bridge feature in our technical docs",
});
# Enable Test Mode
@@ -2,9 +2,9 @@ import { createMetadata, DocImage } from "@doc";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export const metadata = createMetadata({
title: "Webhooks | thirdweb Pay",
title: "Universal Bridge Webhooks Implementation Guide — thirdweb Docs",
description:
"Configure Pay webhooks to get notified at any point during a transaction's lifecycle.",
"Learn how to set up webhooks for web3 payments with thirdweb’s Universal bridge feature: the technical docs",
});
# Webhooks
+60 -81
View File
@@ -270,99 +270,78 @@ export const sidebar: SideBar = {
// Blockchain API
// TODO Overview page?
{
name: "Onchain Interactions",
name: "Universal Bridge",
isCollapsible: false,
links: [
// Pay
{
name: "Bridge & Swap",
name: "Overview",
href: `${paySlug}/overview`,
},
{
name: "Get Started",
href: `${paySlug}/get-started`,
expanded: true,
links: [
{
name: "Overview",
href: `${paySlug}/overview`,
name: "ConnectButton",
href: `${paySlug}/get-started#option-1-connectbutton`,
},
{
name: "Get Started",
href: `${paySlug}/get-started`,
expanded: true,
links: [
{
name: "ConnectButton",
href: `${paySlug}/get-started#option-1-connectbutton`,
},
{
name: "Embed Pay",
href: `${paySlug}/get-started#option-2-embed-pay`,
},
{
name: "Send a Transaction",
href: `${paySlug}/get-started#option-3-send-a-transaction-with-pay`,
},
],
name: "Embed Pay",
href: `${paySlug}/get-started#option-2-embed-pay`,
},
{
name: "Supported Chains",
href: `${paySlug}/supported-chains`,
},
{
name: "Onramp Providers",
href: `${paySlug}/onramp-providers`,
},
{
name: "Fee Sharing",
href: `${paySlug}/fee-sharing`,
},
{
name: "Webhooks",
href: `${paySlug}/webhooks`,
},
{
name: "Testing Pay",
href: `${paySlug}/testing-pay`,
},
{
name: "Guides",
isCollapsible: true,
links: [
{
name: "Accept Direct Payments",
href: `${paySlug}/guides/accept-direct-payments`,
},
{
name: "Build a Custom Experience",
href: `${paySlug}/guides/build-a-custom-experience`,
},
],
},
{
name: "Customization",
isCollapsible: true,
links: [
{
name: "ConnectButton",
href: `${paySlug}/customization/connectbutton`,
},
{
name: "PayEmbed",
href: `${paySlug}/customization/payembed`,
},
{
name: "useSendTransaction",
href: `${paySlug}/customization/send-transaction`,
},
],
},
{
name: "FAQs",
href: `${paySlug}/faqs`,
name: "Send a Transaction",
href: `${paySlug}/get-started#option-3-send-a-transaction-with-pay`,
},
],
},
{
name: "Tutorials",
isCollapsible: true,
links: [
{
name: "Accept Direct Payments",
href: `${paySlug}/guides/accept-direct-payments`,
},
{
name: "Build a Custom Experience",
href: `${paySlug}/guides/build-a-custom-experience`,
},
],
},
{
name: "Customization",
isCollapsible: true,
links: [
{
name: "ConnectButton",
href: `${paySlug}/customization/connectbutton`,
},
{
name: "PayEmbed",
href: `${paySlug}/customization/payembed`,
},
{
name: "useSendTransaction",
href: `${paySlug}/customization/send-transaction`,
},
],
},
{
name: "Webhooks",
href: `${paySlug}/webhooks`,
},
{
name: "Developer Mode",
href: `${paySlug}/testing-pay`,
},
{
name: "FAQs",
href: `${paySlug}/faqs`,
},
],
},
{ separator: true },
+5 -5
View File
@@ -15,11 +15,11 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"lucide-react": "0.476.0",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-themes": "^0.4.4",
"react": "19.0.0",
@@ -35,18 +35,18 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "15.2.0",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"knip": "5.45.0",
"postcss": "8.5.3",
"postcss-load-config": "^6.0.1",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
+2 -2
View File
@@ -43,9 +43,9 @@
"@changesets/changelog-github": "0.5.1",
"@changesets/cli": "2.28.1",
"@manypkg/get-packages": "2.2.2",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"turbo": "2.4.4",
"typescript": "5.7.3"
"typescript": "5.8.2"
},
"pnpm": {
"overrides": {
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/service-utils
## 0.8.13
### Patch Changes
- [#6416](https://github.com/thirdweb-dev/js/pull/6416) [`44bb003`](https://github.com/thirdweb-dev/js/commit/44bb0034a69334f05cfa14132d56e354536e77ff) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] Add "pay" as a usage source
## 0.8.12
### Patch Changes
- [#6409](https://github.com/thirdweb-dev/js/pull/6409) [`67354dc`](https://github.com/thirdweb-dev/js/commit/67354dc848bfb00d2296d5e52990321e9d28abb0) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] remove unneeded cloudflare worker types
## 0.8.11
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/service-utils",
"version": "0.8.11",
"version": "0.8.13",
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
@@ -51,8 +51,8 @@
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250224.0",
"@types/node": "22.13.5",
"typescript": "5.7.3",
"@types/node": "22.13.9",
"typescript": "5.8.2",
"vitest": "3.0.7"
},
"scripts": {
@@ -1,9 +1,9 @@
import type {
ExecutionContext,
KVNamespace,
Request,
Response,
} from "@cloudflare/workers-types";
import type { Request } from "@cloudflare/workers-types";
import type { CoreServiceConfig, TeamAndProjectResponse } from "../core/api.js";
import { authorize } from "../core/authorize/index.js";
import type {
@@ -1,4 +1,4 @@
import { Headers, type Request, fetch } from "@cloudflare/workers-types";
import type { Request } from "@cloudflare/workers-types";
import type { CoreAuthInput } from "../core/types.js";
import type {
ClientUsageV2Event,
@@ -34,7 +34,7 @@ export async function sendUsageV2Events<T extends UsageV2Options>(
// Forward headers from the origin request.
// Determine endpoint and auth header based on provided credentials.
let url: string;
const headers = new Headers(authInput.req.headers);
const headers = new Headers(Object.fromEntries(authInput.req.headers));
headers.set("Content-Type", "application/json");
if (serviceKey) {
// If a service key is provided, call the non-public usage endpoint.
@@ -7,6 +7,7 @@ export const USAGE_V2_SOURCES = [
"sdk",
"storage",
"wallet",
"pay",
] as const;
export type UsageV2Source = (typeof USAGE_V2_SOURCES)[number];
export function getTopicName(source: UsageV2Source) {
+20
View File
@@ -1,5 +1,25 @@
# thirdweb
## 5.90.4
### Patch Changes
- [#6432](https://github.com/thirdweb-dev/js/pull/6432) [`617fe68`](https://github.com/thirdweb-dev/js/commit/617fe68c32888462d09ff335cd7fb2d671e667bf) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow overriding storage for inApp and ecosystem wallets
## 5.90.3
### Patch Changes
- [#6424](https://github.com/thirdweb-dev/js/pull/6424) [`38ae2d3`](https://github.com/thirdweb-dev/js/commit/38ae2d3c6b7d5052974e8a61333997699230ad45) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose getUserOpHash utility function
## 5.90.2
### Patch Changes
- [#6415](https://github.com/thirdweb-dev/js/pull/6415) [`fb446bd`](https://github.com/thirdweb-dev/js/commit/fb446bd0993a0f431fbc67e6142ec20866c4bc65) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - emit onPurchaseSuccess for wallet to wallet transfer flow before execution
- [#6412](https://github.com/thirdweb-dev/js/pull/6412) [`9f1a77f`](https://github.com/thirdweb-dev/js/commit/9f1a77f4c49350018dad0037c4ab675818057264) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow resending OTP on failures
## 5.90.1
### Patch Changes
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.90.1",
"version": "5.90.4",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -218,7 +218,7 @@
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-tooltip": "1.1.8",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"@walletconnect/ethereum-provider": "2.17.5",
"@walletconnect/sign-client": "2.17.5",
"abitype": "1.0.8",
@@ -334,19 +334,19 @@
"@aws-sdk/client-lambda": "3.592.0",
"@aws-sdk/credential-providers": "3.592.0",
"@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "3.2.4",
"@chromatic-com/storybook": "3.2.5",
"@codspeed/vitest-plugin": "4.0.0",
"@coinbase/wallet-mobile-sdk": "1.1.2",
"@mobile-wallet-protocol/client": "0.1.2",
"@react-native-async-storage/async-storage": "2.1.1",
"@size-limit/preset-big-lib": "11.2.0",
"@storybook/addon-essentials": "8.6.0",
"@storybook/addon-interactions": "8.6.0",
"@storybook/addon-links": "8.6.0",
"@storybook/addon-onboarding": "8.6.0",
"@storybook/react": "8.6.0",
"@storybook/react-vite": "8.6.0",
"@storybook/test": "8.6.0",
"@storybook/addon-essentials": "8.6.3",
"@storybook/addon-interactions": "8.6.3",
"@storybook/addon-links": "8.6.3",
"@storybook/addon-onboarding": "8.6.3",
"@storybook/react": "8.6.3",
"@storybook/react-vite": "8.6.3",
"@storybook/test": "8.6.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -375,10 +375,10 @@
"rimraf": "6.0.1",
"sharp": "^0.33.5",
"size-limit": "11.2.0",
"storybook": "8.6.0",
"storybook": "8.6.3",
"typedoc": "0.27.9",
"typedoc-better-json": "0.9.4",
"typescript": "5.7.3",
"typescript": "5.8.2",
"vite": "6.2.0",
"vitest": "3.0.7"
}
@@ -6,7 +6,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
MarketplaceV3: "0x8b0DBCf5b7D01eBB0F24525CE8AB72F16CE4F8C8",
},
[324]: {
MarketplaceV3: "0xBc02441a36Bb4029Cd191b20243c2e41B862F118",
MarketplaceV3: "0x88e7F2bEf0a23073b7Cc2c79D307Dbd9138Fa79c",
},
[11124]: {
MarketplaceV3: "0x2dA4Dd326A6482679547071be21f74685d730504",
@@ -18,7 +18,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
MarketplaceV3: "0x9EB0830B0b10010F2a53383517A7D0B75531Bb1b",
},
[2741]: {
MarketplaceV3: "0x4027561E163a420c4e5Db46E07EBd581CAa8Bb62",
MarketplaceV3: "0x4b14569c7B79DBe686Ac3Ba5996131E7EDaB7a93",
},
};
+1
View File
@@ -10,3 +10,4 @@ export {
resolveArweaveScheme,
type ResolveArweaveSchemeOptions,
} from "../utils/arweave.js";
export type { AsyncStorage } from "../utils/storage/AsyncStorage.js";
@@ -5,6 +5,7 @@ export {
createUnsignedUserOp,
signUserOp,
createAndSignUserOp,
getUserOpHash,
} from "../../wallets/smart/lib/userop.js";
export {
@@ -17,10 +17,8 @@ import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value
import { toEther } from "../../utils/units.js";
import { generateMerkleTreeInfoERC1155 } from "../airdrop/write/merkleInfoERC1155.js";
import { name } from "../common/read/name.js";
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js";
import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js";
import { totalSupply } from "./__generated__/IERC1155/read/totalSupply.js";
import { nextTokenIdToMint } from "./__generated__/IERC1155Enumerable/read/nextTokenIdToMint.js";
import { canClaim } from "./drops/read/canClaim.js";
import { getActiveClaimCondition } from "./drops/read/getActiveClaimCondition.js";
@@ -472,63 +470,6 @@ describe.runIf(process.env.TW_SECRET_KEY)(
expect(isGetNFTsSupported(selectors)).toBe(true);
});
/**
* This is to document the behavior where one can claim without paying if the claiming address
* is the same as the PrimaryRecipientAddress, because of this Solidity code:
* ```solidity
* // CurrencyTransferLib.sol
* function safeTransferERC20(address _currency, address _from, address _to, uint256 _amount) internal {
* if (_from == _to) {
* return;
* }
* ...
* }
* ```
*/
it("address that is the same with PrimaryFeeRecipient can claim without paying ERC20", async () => {
const tokenAddress = await deployERC20Contract({
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
account: TEST_ACCOUNT_C,
type: "TokenERC20",
params: {
name: "token20",
contractURI: TEST_CONTRACT_URI,
},
});
const tokenId = 5n;
const setClaimTx = setClaimConditions({
contract,
tokenId,
phases: [
{
maxClaimableSupply: 100n,
maxClaimablePerWallet: 100n,
currencyAddress: tokenAddress,
price: 1000,
startTime: new Date(),
},
],
});
await sendAndConfirmTransaction({
transaction: setClaimTx,
account: TEST_ACCOUNT_C,
});
const transaction = claimTo({
contract,
tokenId,
quantity: 50n,
to: TEST_ACCOUNT_C.address,
});
await sendAndConfirmTransaction({
transaction,
account: TEST_ACCOUNT_C,
});
const supplyCount = await totalSupply({ contract, id: tokenId });
expect(supplyCount).toBe(50n);
});
it("getActiveClaimCondition should work", async () => {
// Create a public allowlist claim phase
const snapshot = [
@@ -300,7 +300,6 @@ export function ConnectButton(props: ConnectButtonProps) {
usePreloadWalletProviders({
wallets,
client: props.client,
});
// Add props.chain and props.chains to defined chains store
@@ -215,7 +215,6 @@ export function ConnectEmbed(props: ConnectEmbedProps) {
usePreloadWalletProviders({
wallets,
client: props.client,
});
const modalSize = useMemo(() => {
@@ -6,12 +6,18 @@ import type { ThirdwebClient } from "../../../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
import { getContract } from "../../../../../../../contract/contract.js";
import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js";
import {
type GetCurrencyMetadataResult,
getCurrencyMetadata,
} from "../../../../../../../extensions/erc20/read/getCurrencyMetadata.js";
import { approve } from "../../../../../../../extensions/erc20/write/approve.js";
import { transfer } from "../../../../../../../extensions/erc20/write/transfer.js";
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import { getBuyWithCryptoTransfer } from "../../../../../../../pay/buyWithCrypto/getTransfer.js";
import { sendAndConfirmTransaction } from "../../../../../../../transaction/actions/send-and-confirm-transaction.js";
import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js";
import { prepareTransaction } from "../../../../../../../transaction/prepare-transaction.js";
import type { TransactionReceipt } from "../../../../../../../transaction/types.js";
import type { Address } from "../../../../../../../utils/address.js";
import { toWei } from "../../../../../../../utils/units.js";
import { iconSize } from "../../../../../../core/design-system/index.js";
@@ -42,6 +48,7 @@ type TransferConfirmationScreenProps = {
tokenAmount: string;
transactionMode?: boolean;
payOptions?: PayUIOptions;
onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
};
export function TransferConfirmationScreen(
@@ -202,10 +209,33 @@ export function TransferConfirmationScreen(
to: receiverAddress,
amount: tokenAmount,
});
await sendAndConfirmTransaction({
account: props.payer.account,
transaction,
});
const [txResult, tokenMetadata] = await Promise.all([
sendAndConfirmTransaction({
account: props.payer.account,
transaction,
}),
getCurrencyMetadata({
contract: getContract({
address: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
chain: chain,
client: client,
}),
}),
]);
// its the last step before the transaction, so propagate onPurchaseSuccess here
props.onSuccess?.(
transferBuyWithCryptoQuote({
token,
chain,
tokenMetadata,
tokenAmount,
fromAddress: payer.account.address,
toAddress: receiverAddress,
transaction: txResult,
}),
);
// switch to execute step
setStep("execute");
setStatus({ id: "idle" });
@@ -299,3 +329,85 @@ export function TransferConfirmationScreen(
</Container>
);
}
function transferBuyWithCryptoQuote(args: {
token: ERC20OrNativeToken;
chain: Chain;
tokenMetadata: GetCurrencyMetadataResult;
tokenAmount: string;
fromAddress: string;
toAddress: string;
transaction: TransactionReceipt;
}): BuyWithCryptoStatus {
const {
token,
chain,
tokenMetadata,
tokenAmount,
fromAddress,
toAddress,
transaction,
} = args;
return {
status: "COMPLETED",
subStatus: "SUCCESS",
swapType: "TRANSFER",
quote: {
createdAt: new Date().toISOString(),
fromToken: {
chainId: chain.id,
tokenAddress: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
decimals: tokenMetadata.decimals,
symbol: tokenMetadata.symbol,
name: tokenMetadata.name,
priceUSDCents: 0,
},
toToken: {
chainId: chain.id,
tokenAddress: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
decimals: tokenMetadata.decimals,
symbol: tokenMetadata.symbol,
name: tokenMetadata.name,
priceUSDCents: 0,
},
fromAmountWei: toWei(tokenAmount).toString(),
fromAmount: tokenAmount,
toAmountWei: toWei(tokenAmount).toString(),
toAmount: tokenAmount,
toAmountMin: tokenAmount,
toAmountMinWei: toWei(tokenAmount).toString(),
estimated: {
feesUSDCents: 0,
gasCostUSDCents: 0,
slippageBPS: 0,
toAmountMinUSDCents: 0,
toAmountUSDCents: 0,
fromAmountUSDCents: 0,
durationSeconds: 0,
},
},
fromAddress,
toAddress,
source: {
transactionHash: transaction.transactionHash,
amount: tokenAmount,
amountWei: toWei(tokenAmount).toString(),
amountUSDCents: 0,
completedAt: new Date().toISOString(),
token: {
chainId: chain.id,
tokenAddress: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
decimals: tokenMetadata.decimals,
symbol: tokenMetadata.symbol,
name: tokenMetadata.name,
priceUSDCents: 0,
},
},
};
}
@@ -1,15 +1,10 @@
import { useQueries } from "@tanstack/react-query";
import type { ThirdwebClient } from "../../../client/client.js";
import { COINBASE } from "../../../wallets/constants.js";
import { isEcosystemWallet } from "../../../wallets/ecosystem/is-ecosystem-wallet.js";
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
import type { CreateWalletArgs } from "../../../wallets/wallet-types.js";
import type { EcosystemWalletId } from "../../../wallets/wallet-types.js";
export function usePreloadWalletProviders({
client,
wallets,
}: { client: ThirdwebClient; wallets: Wallet[] }) {
export function usePreloadWalletProviders({ wallets }: { wallets: Wallet[] }) {
useQueries({
queries: wallets
.filter(
@@ -29,49 +24,6 @@ export function usePreloadWalletProviders({
// return _something_
return true;
}
case "inApp" === w.id: {
const [
{ InAppWebConnector },
{ getOrCreateInAppWalletConnector },
] = await Promise.all([
import("../../../wallets/in-app/web/lib/web-connector.js"),
import("../../../wallets/in-app/core/wallet/in-app-core.js"),
]);
await getOrCreateInAppWalletConnector(client, async (client) => {
return new InAppWebConnector({
client,
});
});
// return _something_
return true;
}
case isEcosystemWallet(w.id): {
const [
{ InAppWebConnector },
{ getOrCreateInAppWalletConnector },
] = await Promise.all([
import("../../../wallets/in-app/web/lib/web-connector.js"),
import("../../../wallets/in-app/core/wallet/in-app-core.js"),
]);
const ecosystemWallet = w as Wallet<EcosystemWalletId>; // we know this is an ecosystem wallet
await getOrCreateInAppWalletConnector(
client,
async (client) => {
return new InAppWebConnector({
client,
ecosystem: {
id: ecosystemWallet.id,
partnerId: ecosystemWallet.getConfig()?.partnerId,
},
});
},
{
id: ecosystemWallet.id,
partnerId: ecosystemWallet.getConfig()?.partnerId,
},
);
return true;
}
// potentially add more wallets here
default: {
return false;
@@ -276,7 +276,7 @@ export function OTPLoginUI(props: {
{!isWideModal && <Line />}
<Container p={isWideModal ? undefined : "lg"}>
<Container p={isWideModal ? undefined : "lg"} gap="xs">
{accountStatus === "error" && (
<Text size="sm" center color="danger">
{locale.emailLoginScreen.failedToSendCode}
@@ -297,7 +297,7 @@ export function OTPLoginUI(props: {
</Container>
)}
{accountStatus === "sent" && (
{accountStatus !== "sending" && (
<LinkButton onClick={sendEmailOrSms} type="button">
{locale.emailLoginScreen.resendCode}
</LinkButton>
@@ -74,16 +74,12 @@ describe.runIf(process.env.TW_SECRET_KEY)("prepareengineTransaction", () => {
const { message, messageType, signature } = result;
expect(isHex(signature)).toBe(true);
expect(message).toMatchInlineSnapshot(`
{
"data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"gas": 64340n,
"nonce": 0n,
"to": "${erc20Contract.address}",
"value": 0n,
}
`);
expect(message.data).toBe(
"0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
);
expect(message.from).toBe(TEST_ACCOUNT_A.address);
expect(message.to).toBe(erc20Contract.address);
expect(message.value).toBe(0n);
expect(messageType).toMatchInlineSnapshot(`"forward"`);
});

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