Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
5c6dd874e0 Version Packages (#7577)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-09 16:55:48 -07:00
a9e47e4759 Replace PayEmbed with BuyWidget code (#7574)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-09 15:47:29 -07:00
bfd728a9d1 Add branding hide prop to widgets (#7575)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-09 15:46:29 -07:00
saminacodes 381a2823d7 [Docs] Remove Nebula from portal navigation (#7576)
```
<!--

## title your PR with this format: "[Portal] Feature: Remove Nebula from portal navigation"

## Notes for the reviewer

This PR removes all user-facing references to Nebula from the portal navigation and homepage, as requested.

-   **Key point:** All Nebula pages and underlying content remain untouched; only navigation links and homepage sections were removed.
-   Removed Nebula from the main header navigation and the APIs dropdown.
-   Removed Nebula sections from the homepage (API Reference and Learning Resources).
-   Updated the Vault section icon on the homepage from `NebulaIcon` (which was removed) to `ConnectIcon`.

## How to test

1.  Run the portal locally.
2.  Verify "Nebula" is no longer present in the main navigation bar or the "APIs" dropdown.
3.  On the homepage (`/`), confirm that the "Nebula" cards are removed from the "API Reference" and "Learning Resources" sections.
4.  Confirm that the "Vault" card in the "Learning Resources" section now uses the `ConnectIcon`.
5.  Run `npm run portal:build` to ensure the project builds successfully.

-->
```

---

[Slack Thread](https://thirdwebdev.slack.com/archives/C04DYC3G5NZ/p1752094214240499?thread_ts=1752094214.240499&cid=C04DYC3G5NZ)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on removing references to `Nebula` from the application, including links and UI components, while retaining and emphasizing `Insight` and `Vault`.

### Detailed summary
- Removed `Nebula` link from `connectLinks` in `Header.tsx`.
- Deleted `Nebula` API reference card from `ReferenceSection` in `page.tsx`.
- Eliminated `Nebula` card from `LearningResourcesSection` in `page.tsx`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * None.

* **Bug Fixes**
  * None.

* **Refactor**
  * Removed all references to the "Nebula" feature from navigation menus and resource sections.
  * Updated the icon for the "Vault" article card.

* **Chores**
  * Cleaned up unused imports related to "Nebula".

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 21:53:40 +00:00
saminacodes 2111b8b12c Rename Universal Bridge to Payments (#7571)
```
<!--

## title your PR with this format: "[Dashboard] Feature: Rename Universal Bridge to Payments"

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

## Notes for the reviewer

This PR renames all instances of "Universal Bridge" to "Payments" across the projects dashboard. This is a branding update and does not change any underlying functionality or URLs.

Changes include:
- Sidebar navigation label
- Page titles and descriptions
- Callouts and sub-text (e.g., analytics, FTUX)
- Webhooks section (tab name and heading)

## How to test

1. Navigate to any project dashboard.
2. Verify the sidebar now shows "Payments" instead of "Universal Bridge".
3. Click on "Payments" in the sidebar and confirm the page title and description are updated.
4. Check the "Analytics" page to ensure "Universal Bridge" references are updated to "Payments".
5. Go to "Webhooks" and then the "Payments" tab to confirm the heading and tab name are correct.
6. Verify the FTUX section also reflects "Payments".

-->
```

---

[Slack Thread](https://thirdwebdev.slack.com/archives/C08GEMP858V/p1752037029611589?thread_ts=1752037029.611589&cid=C08GEMP858V)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming references from "Universal Bridge" to "Payments" throughout the codebase, reflecting a change in terminology for a feature that facilitates payments.

### Detailed summary
- Updated `name` in `layout.tsx` from "Universal Bridge" to "Payments".
- Changed `label` in `ProjectSidebarLayout.tsx` from "Universal Bridge" to "Payments".
- Modified `title` in `ProjectFTUX.tsx` from "Universal Bridge" to "Payments".
- Changed heading in `page.tsx` from "Universal Bridge Webhooks" to "Payments Webhooks".
- Updated description in `analytics/page.tsx` from "Universal Bridge" to "Payments".
- Changed description in `sidebar/page.tsx` from "Universal Bridge" to "Payments".
- Updated heading in `universal-bridge/page.tsx` from "Get Started with Universal Bridge" to "Get Started with Payments".
- Changed heading in `universal-bridge/layout.tsx` from "Universal Bridge" to "Payments".
- Updated description in `universal-bridge/layout.tsx` to reflect "Payments" instead of "Universal Bridge".

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

<!-- end pr-codex -->
2025-07-09 05:13:22 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1665e4d534 Version Packages (#7568)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-08 17:18:24 -07:00
Manan TankandGitHub 692cedf921 [TOOL-4987] SDK: Restore CSS resets, remove biome rules for sorting attributes and properties (#7567) 2025-07-08 15:47:22 -07:00
e1f4901b80 [SDK] Remove page and set up redirect (#7565)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-08 12:06:36 -07:00
MananTank 028f871d40 [TOOL-4972] Dashboard: Add field to configure admins in NFT asset creation (#7542)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the NFT creation process by adding the ability to set admin addresses and refining various components for better user experience and validation.

### Detailed summary
- Added `"set-admins"` step to the NFT asset type.
- Introduced `AdminAddressesFieldset` for managing admin addresses.
- Updated forms to include admin validation.
- Improved UI components with consistent styling.
- Enhanced error handling during admin setting.

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

<!-- end pr-codex -->

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

* **New Features**
  * Added the ability to specify multiple admin wallet addresses when creating an NFT collection, with dynamic form fields and validation.
  * NFT launch workflow now includes a step to assign admin roles to specified addresses.
  * NFT collection info and launch dialogs now display the list of assigned admins.

* **Improvements**
  * The NFT and token creation forms now automatically set the default blockchain network based on the active wallet chain.
  * NFT upload form includes default values for price and supply fields.
  * Visual spacing adjustments in social URL and token launch dialogs for improved UI consistency.

* **Bug Fixes**
  * Ensured at least one admin address is required when creating an NFT collection.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 15:37:47 +00:00
2821b4eeb7 Make contract webhook addresses mandatory (#7544)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-08 15:20:49 +02:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
adfa56b2a2 Version Packages (#7555)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-07 18:54:19 -07:00
YashandGitHub 415ad90986 feat: add T-Rex testnet chain (#7540) 2025-07-07 18:17:02 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
afcb7bba84 chore(deps): bump zod from 3.25.71 to 3.25.75 (#7552)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 18:15:59 -07:00
Joaquim VergesandGitHub 10c3b6f27f [SDK] Respect fee payer in token payment selection quotes (#7556) 2025-07-07 18:14:50 -07:00
Joaquim VergesandGitHub c64446306e [SDK] Use new BuyWidget in wallet details modal (#7551) 2025-07-07 17:18:28 -07:00
saminaandGitHub 2136c7ba4a [Docs] Wallets Overview (#7543) 2025-07-07 16:21:33 -07:00
MananTank 67c3d861fe [TOOL-4986] Dashboard: Fix ERC20 asset page crash (#7541)
<!--

## 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 improving error handling by adding `try-catch` blocks around asynchronous calls in the `getContractCreator` function. This change ensures that if an error occurs during the execution of `owner` or `getRoleMember`, the function will return `null` instead of throwing an unhandled error.

### Detailed summary
- Wrapped the calls to `owner` and `getRoleMember` in a `try-catch` block.
- Changed the calls to `owner` and `getRoleMember` to use `await` for proper asynchronous handling.
- Ensured that if an error occurs, the function returns `null` instead of failing silently.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved error handling when retrieving contract creator information, ensuring failures no longer cause crashes and instead return a safe null value.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-07 14:15:01 +00:00
d2c12cc6ef Create session key guide and update sidebar (#7538)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-07 03:30:38 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c1b2e10f34 Version Packages (#7539)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-07 03:29:31 -07:00
Joaquim VergesandGitHub f48d8006bc [SDK] Optimize payment token fetching in payment widgets (#7536) 2025-07-07 02:50:25 -07:00
Joaquim VergesandGitHub 20e323d666 [Dashboard] Fix smart account options validation for non-EIP4337 execution modes (#7537) 2025-07-07 01:20:11 -07:00
128 changed files with 1716 additions and 8837 deletions
+1 -1
View File
@@ -81,7 +81,7 @@
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.5",
"vaul": "^1.1.2",
"zod": "3.25.71"
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
+5 -1
View File
@@ -362,7 +362,11 @@ export function reportAssetCreationFailed(
properties: { contractType: AssetContractType; error: string } & (
| {
assetType: "nft";
step: "deploy-contract" | "mint-nfts" | "set-claim-conditions";
step:
| "deploy-contract"
| "mint-nfts"
| "set-claim-conditions"
| "set-admins";
}
| {
assetType: "coin";
@@ -9,19 +9,23 @@ export async function getContractCreator(
contract: ThirdwebContract,
functionSelectors: string[],
) {
if (isOwnerSupported(functionSelectors)) {
return owner({
contract,
});
}
try {
if (isOwnerSupported(functionSelectors)) {
return await owner({
contract,
});
}
if (isGetRoleAdminSupported(functionSelectors)) {
return getRoleMember({
contract,
index: BigInt(0),
role: "admin",
});
}
if (isGetRoleAdminSupported(functionSelectors)) {
return await getRoleMember({
contract,
index: BigInt(0),
role: "admin",
});
}
return null;
return null;
} catch {
return null;
}
}
@@ -367,7 +367,7 @@ function AppHighlightsCard({
color: "hsl(var(--chart-2))",
emptyContent: (
<EmptyStateContent
description="Onramp, swap, and bridge with thirdweb's Universal Bridge."
description="Onramp, swap, and bridge with thirdweb's Payments."
link="https://portal.thirdweb.com/connect/pay/overview"
metric="Payments"
/>
@@ -231,22 +231,24 @@ export function AuthOptionsForm({
}
let smartAccountOptions: Ecosystem["smartAccountOptions"] | null = null;
if (data.useSmartAccount && data.executionMode === "EIP4337") {
let accountFactoryAddress: string;
switch (data.accountFactoryType) {
case "v0.6":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_6;
break;
case "v0.7":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_7;
break;
case "custom":
if (!data.customAccountFactoryAddress) {
toast.error("Please enter a custom account factory address");
return;
}
accountFactoryAddress = data.customAccountFactoryAddress;
break;
if (data.useSmartAccount) {
let accountFactoryAddress: string | undefined;
if (data.executionMode === "EIP4337") {
switch (data.accountFactoryType) {
case "v0.6":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_6;
break;
case "v0.7":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_7;
break;
case "custom":
if (!data.customAccountFactoryAddress) {
toast.error("Please enter a custom account factory address");
return;
}
accountFactoryAddress = data.customAccountFactoryAddress;
break;
}
}
smartAccountOptions = {
@@ -257,7 +257,7 @@ function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
"Bridge, swap, and purchase cryptocurrencies with any fiat options or tokens via cross-chain routing",
href: `/team/${props.teamSlug}/${props.projectSlug}/universal-bridge`,
icon: PayIcon,
title: "Universal Bridge",
title: "Payments",
},
];
@@ -75,7 +75,7 @@ export function ProjectSidebarLayout(props: {
{
href: `${layoutPath}/universal-bridge`,
icon: PayIcon,
label: "Universal Bridge",
label: "Payments",
},
{
href: `${layoutPath}/tokens`,
@@ -435,7 +435,7 @@ function AppHighlightsCard({
color: "hsl(var(--chart-2))",
emptyContent: (
<EmptyStateContent
description="Onramp, swap, and bridge with thirdweb's Universal Bridge."
description="Onramp, swap, and bridge with thirdweb's Payments."
link="https://portal.thirdweb.com/connect/pay/overview"
metric="Payments"
/>
@@ -34,7 +34,7 @@ export function SocialUrlsFieldset<T extends WithSocialUrls>(props: {
<h2 className="mb-2 font-medium text-sm">Social URLs</h2>
{fields.length > 0 && (
<div className="mb-5 space-y-4">
<div className="mb-4 space-y-3">
{fields.map((field, index) => (
<div
className="flex gap-3 max-sm:mb-6 max-sm:border-b max-sm:border-dashed max-sm:pb-6"
@@ -0,0 +1,115 @@
"use client";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { type UseFormReturn, useFieldArray } from "react-hook-form";
import { useActiveAccount } from "thirdweb/react";
import { Button } from "@/components/ui/button";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
type WithAdmins = {
admins: {
address: string;
}[];
};
export function AdminAddressesFieldset<T extends WithAdmins>(props: {
form: UseFormReturn<T>;
}) {
// T contains all properties of WithAdmins, so this is ok
const form = props.form as unknown as UseFormReturn<WithAdmins>;
const account = useActiveAccount();
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "admins",
});
const handleAddAddress = () => {
append({ address: "" });
};
const handleRemoveAddress = (index: number) => {
const field = fields[index];
if (field?.address === account?.address) {
return; // Don't allow removing the connected address
}
remove(index);
};
return (
<div className="border-t border-dashed px-4 py-6 lg:px-6">
<div className="mb-3">
<h2 className="mb-1 font-medium text-sm">Admins</h2>
<p className="text-sm text-muted-foreground">
These wallets will have authority on the token
</p>
</div>
{fields.length > 0 && (
<div className="mb-4 space-y-3">
{fields.map((field, index) => (
<div
className="flex gap-3 max-sm:mb-6 max-sm:border-b max-sm:border-dashed max-sm:pb-6"
key={field.id}
>
<div className="flex flex-1 flex-col gap-3 lg:flex-row">
<FormField
control={form.control}
name={`admins.${index}.address`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input
{...field}
aria-label="Admin Address"
disabled={field.value === account?.address}
placeholder="0x..."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button
className="rounded-full"
disabled={field.address === account?.address}
onClick={() => handleRemoveAddress(index)}
size="icon"
type="button"
variant="outline"
>
<Trash2Icon className="h-4 w-4" />
<span className="sr-only">Remove</span>
</Button>
</div>
))}
</div>
)}
<Button
className="h-auto gap-1.5 rounded-full px-3 py-1.5 text-xs"
onClick={handleAddAddress}
size="sm"
type="button"
variant="outline"
>
<PlusIcon className="size-3.5" />
Add Admin
</Button>
{form.watch("admins").length === 0 && (
<p className="text-sm text-destructive mt-2">
At least one admin address is required
</p>
)}
</div>
);
}
@@ -22,6 +22,22 @@ export const socialUrlsSchema = z.array(
}),
);
export const addressArraySchema = z.array(
z.object({
address: z.string().refine(
(value) => {
if (isAddress(value)) {
return true;
}
return false;
},
{
message: "Invalid address",
},
),
}),
);
export const addressSchema = z.string().refine(
(value) => {
if (isAddress(value)) {
@@ -1,9 +1,15 @@
import { isAddress } from "thirdweb";
import * as z from "zod";
import { socialUrlsSchema } from "../../_common/schema";
import {
addressArraySchema,
addressSchema,
socialUrlsSchema,
} from "../../_common/schema";
import type { NFTMetadataWithPrice } from "../upload-nfts/batch-upload/process-files";
export const nftCollectionInfoFormSchema = z.object({
admins: addressArraySchema.refine((addresses) => addresses.length > 0, {
message: "At least one admin is required",
}),
chain: z.string().min(1, "Chain is required"),
description: z.string().optional(),
image: z.instanceof(File).optional(),
@@ -12,14 +18,6 @@ export const nftCollectionInfoFormSchema = z.object({
symbol: z.string(),
});
const addressSchema = z.string().refine((value) => {
if (isAddress(value)) {
return true;
}
return false;
});
export const nftSalesSettingsFormSchema = z.object({
primarySaleRecipient: addressSchema,
royaltyBps: z.coerce.number().min(0).max(10000),
@@ -37,6 +35,14 @@ export type CreateNFTCollectionAllValues = {
};
export type CreateNFTCollectionFunctions = {
setAdmins: (values: {
contractAddress: string;
contractType: "DropERC721" | "DropERC1155";
admins: {
address: string;
}[];
chain: string;
}) => Promise<void>;
erc721: {
deployContract: (values: CreateNFTCollectionAllValues) => Promise<{
contractAddress: string;
@@ -10,6 +10,7 @@ import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { AdminAddressesFieldset } from "../../_common/admin-addresses-fieldset";
import { SocialUrlsFieldset } from "../../_common/SocialUrls";
import { StepCard } from "../../_common/step-card";
import type { NFTCollectionInfoFormValues } from "../_common/form";
@@ -126,6 +127,8 @@ export function NFTCollectionInfoFieldset(props: {
</div>
<SocialUrlsFieldset form={form} />
<AdminAddressesFieldset form={form} />
</StepCard>
</form>
</Form>
@@ -8,7 +8,7 @@ import {
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
} from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import { reportAssetCreationStepConfigured } from "@/analytics/report";
import type { Team } from "@/api/team";
import {
@@ -163,11 +163,16 @@ export function CreateNFTPageUI(props: {
}
function useNFTCollectionInfoForm() {
const chain = useActiveWalletChain();
const account = useActiveAccount();
return useForm<NFTCollectionInfoFormValues>({
resolver: zodResolver(nftCollectionInfoFormSchema),
reValidateMode: "onChange",
values: {
chain: "1",
defaultValues: {
admins: [
{
address: account?.address || "",
},
],
chain: chain?.id.toString() || "1",
description: "",
image: undefined,
name: "",
@@ -183,5 +188,7 @@ function useNFTCollectionInfoForm() {
],
symbol: "",
},
resolver: zodResolver(nftCollectionInfoFormSchema),
reValidateMode: "onChange",
});
}
@@ -18,6 +18,7 @@ import {
lazyMint as lazyMint1155,
setClaimConditions as setClaimConditions1155,
} from "thirdweb/extensions/erc1155";
import { grantRole } from "thirdweb/extensions/permissions";
import { useActiveAccount } from "thirdweb/react";
import { maxUint256 } from "thirdweb/utils";
import { revalidatePathAction } from "@/actions/revalidate";
@@ -332,6 +333,60 @@ export function CreateNFTPage(props: {
}
}
async function handleSetAdmins(params: {
contractAddress: string;
contractType: "DropERC721" | "DropERC1155";
admins: {
address: string;
}[];
chain: string;
}) {
const { contract, activeAccount } = getContractAndAccount({
chain: params.chain,
});
// remove the current account from the list - its already an admin, don't have to add it again
const adminsToAdd = params.admins.filter(
(admin) => admin.address !== activeAccount.address,
);
const encodedTxs = await Promise.all(
adminsToAdd.map((admin) => {
const tx = grantRole({
contract,
role: "admin",
targetAccountAddress: admin.address,
});
return encode(tx);
}),
);
const tx = multicall({
contract,
data: encodedTxs,
});
try {
await sendAndConfirmTransaction({
account: activeAccount,
transaction: tx,
});
} catch (e) {
const errorMessage = parseError(e);
console.error(errorMessage);
reportAssetCreationFailed({
assetType: "nft",
contractType: params.contractType,
error: errorMessage,
step: "set-admins",
});
throw e;
}
}
return (
<CreateNFTPageUI
{...props}
@@ -349,7 +404,6 @@ export function CreateNFTPage(props: {
formValues,
});
},
setClaimConditions: async (formValues) => {
return handleSetClaimConditionsERC721({
formValues,
@@ -373,6 +427,7 @@ export function CreateNFTPage(props: {
return handleSetClaimConditionsERC1155(params);
},
},
setAdmins: handleSetAdmins,
}}
onLaunchSuccess={() => {
revalidatePathAction(
@@ -8,7 +8,12 @@ import {
import Link from "next/link";
import { useMemo, useRef, useState } from "react";
import { defineChain, type ThirdwebClient } from "thirdweb";
import { TokenProvider, TokenSymbol, useActiveWallet } from "thirdweb/react";
import {
TokenProvider,
TokenSymbol,
useActiveAccount,
useActiveWallet,
} from "thirdweb/react";
import {
reportAssetCreationFailed,
reportAssetCreationSuccessful,
@@ -41,6 +46,7 @@ import type {
const stepIds = {
"deploy-contract": "deploy-contract",
"mint-nfts": "mint-nfts",
"set-admins": "set-admins",
"set-claim-conditions": "set-claim-conditions",
} as const;
@@ -61,7 +67,8 @@ export function LaunchNFT(props: {
const [isModalOpen, setIsModalOpen] = useState(false);
const activeWallet = useActiveWallet();
const walletRequiresApproval = activeWallet?.id !== "inApp";
const [contractLink, setContractLink] = useState<string | null>(null);
const account = useActiveAccount();
const contractAddressRef = useRef<string | null>(null);
function updateStatus(
index: number,
@@ -108,6 +115,19 @@ export function LaunchNFT(props: {
},
];
if (
account &&
formValues.collectionInfo.admins.some(
(admin) => admin.address !== account.address,
)
) {
initialSteps.push({
id: stepIds["set-admins"],
label: "Set admins",
status: { type: "idle" },
});
}
setSteps(initialSteps);
setIsModalOpen(true);
executeSteps(initialSteps, 0);
@@ -134,13 +154,15 @@ export function LaunchNFT(props: {
return shouldDeployERC721 ? "erc721" : "erc1155";
}, [formValues.nfts]);
const contractLink = contractAddressRef.current
? `/team/${props.teamSlug}/${props.projectSlug}/contract/${formValues.collectionInfo.chain}/${contractAddressRef.current}`
: null;
async function executeStep(steps: MultiStepState<StepId>[], stepId: StepId) {
if (stepId === "deploy-contract") {
const result =
await props.createNFTFunctions[ercType].deployContract(formValues);
setContractLink(
`/team/${props.teamSlug}/${props.projectSlug}/contract/${formValues.collectionInfo.chain}/${result.contractAddress}`,
);
contractAddressRef.current = result.contractAddress;
} else if (stepId === "set-claim-conditions") {
if (ercType === "erc721") {
await props.createNFTFunctions.erc721.setClaimConditions(formValues);
@@ -185,6 +207,18 @@ export function LaunchNFT(props: {
}
} else if (stepId === "mint-nfts") {
await props.createNFTFunctions[ercType].lazyMintNFTs(formValues);
} else if (stepId === "set-admins") {
// this is type guard, this can never happen
if (!contractAddressRef.current) {
throw new Error("Contract address not set");
}
await props.createNFTFunctions.setAdmins({
admins: formValues.collectionInfo.admins,
chain: formValues.collectionInfo.chain,
contractAddress: contractAddressRef.current,
contractType: ercType === "erc721" ? "DropERC721" : "DropERC1155",
});
}
}
@@ -304,7 +338,7 @@ export function LaunchNFT(props: {
dialogCloseClassName="hidden"
>
<div className="flex flex-col gap-6 p-6">
<DialogHeader>
<DialogHeader className="space-y-0.5">
<DialogTitle className="font-semibold text-xl tracking-tight">
Status
</DialogTitle>
@@ -412,6 +446,24 @@ export function LaunchNFT(props: {
}
/>
</OverviewField>
<OverviewField
name={
formValues.collectionInfo.admins.length > 1 ? "Admins" : "Admin"
}
>
<div className="space-y-0.5">
{formValues.collectionInfo.admins.map((admin) => (
<WalletAddress
address={admin.address}
className="text-foreground text-sm h-auto py-1"
client={props.client}
iconClassName="size-3.5"
key={admin.address}
/>
))}
</div>
</OverviewField>
</div>
</div>
</div>
@@ -4,7 +4,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeftIcon, ArrowRightIcon, AsteriskIcon } from "lucide-react";
import { useId } from "react";
import { useForm } from "react-hook-form";
import type { ThirdwebClient } from "thirdweb";
import {
getAddress,
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
} from "thirdweb";
import { FileInput } from "@/components/blocks/FileInput";
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { TokenSelector } from "@/components/blocks/TokenSelector";
@@ -32,6 +36,8 @@ import type { NFTMetadataWithPrice } from "../batch-upload/process-files";
import { nftWithPriceSchema } from "../schema";
import { AttributesFieldset } from "./attributes";
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
export function SingleUploadNFT(props: {
client: ThirdwebClient;
onNext: () => void;
@@ -46,6 +52,9 @@ export function SingleUploadNFT(props: {
description: "",
image: undefined,
name: "",
price_amount: "1",
price_currency: nativeTokenAddress,
supply: "1",
},
resolver: zodResolver(nftWithPriceSchema),
reValidateMode: "onChange",
@@ -8,6 +8,7 @@ import {
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
} from "thirdweb";
import { useActiveWalletChain } from "thirdweb/react";
import { reportAssetCreationStepConfigured } from "@/analytics/report";
import type { Team } from "@/api/team";
import {
@@ -44,12 +45,11 @@ export function CreateTokenAssetPageUI(props: {
const [step, setStep] = useState<"token-info" | "distribution" | "launch">(
"token-info",
);
const activeWalletChain = useActiveWalletChain();
const tokenInfoForm = useForm<TokenInfoFormValues>({
resolver: zodResolver(tokenInfoFormSchema),
reValidateMode: "onChange",
values: {
chain: "1",
defaultValues: {
chain: activeWalletChain?.id.toString() || "1",
description: "",
image: undefined,
name: "",
@@ -65,6 +65,8 @@ export function CreateTokenAssetPageUI(props: {
],
symbol: "",
},
resolver: zodResolver(tokenInfoFormSchema),
reValidateMode: "onChange",
});
const tokenDistributionForm = useForm<TokenDistributionFormValues>({
@@ -262,7 +262,7 @@ export function LaunchTokenStatus(props: {
dialogCloseClassName="hidden"
>
<div className="flex flex-col gap-6 p-6">
<DialogHeader>
<DialogHeader className="space-y-0.5">
<DialogTitle className="font-semibold text-xl tracking-tight">
Status
</DialogTitle>
@@ -1,13 +1,20 @@
export const embedCode = (clientId: string) => `\
import { createThirdwebClient } from "thirdweb";
import { PayEmbed } from "thirdweb/react";
import { BuyWidget } from "thirdweb/react";
import { ethereum } from "thirdweb/chains";
const client = createThirdwebClient({
clientId: "${clientId}",
});
export default function App() {
return <PayEmbed client={client} />;
return (
<BuyWidget
client={client}
chain={ethereum}
amount="0.1"
/>
);
}`;
export const sdkCode = (clientId: string) => `\
@@ -25,10 +25,10 @@ export default async function Layout(props: {
<div className="pt-4 lg:pt-6">
<div className="container max-w-7xl">
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
Universal Bridge
Payments
</h1>
<p className="max-w-3xl text-muted-foreground text-sm leading-relaxed">
Universal Bridge allows your users to bridge, swap, and purchase
Payments allows your users to bridge, swap, and purchase
cryptocurrencies and execute transactions with any fiat options or
tokens via cross-chain routing.{" "}
<UnderlineLink
@@ -63,9 +63,7 @@ export default async function Page(props: {
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with Universal Bridge
</h3>
<h3 className="font-medium text-lg">Get Started with Payments</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and chain.
</p>
@@ -109,7 +109,9 @@ export function CreateContractWebhookButton({
await handleSubmit(data);
} catch (error) {
toast.error(
`Failed to process webhook: ${error instanceof Error ? error.message : String(error)}`,
`Failed to process webhook: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
};
@@ -142,7 +144,9 @@ export function CreateContractWebhookButton({
router.refresh();
} catch (error) {
toast.error(
`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`,
`Failed to create webhook: ${
error instanceof Error ? error.message : String(error)
}`,
);
} finally {
setIsLoading(false);
@@ -173,7 +177,11 @@ export function CreateContractWebhookButton({
if (filterType === "event") {
result = await formHook.trigger(["chainIds", "addresses"]);
} else {
result = await formHook.trigger(["chainIds", "toAddresses"]);
result = await formHook.trigger([
"chainIds",
"toAddresses",
"fromAddresses",
]);
}
if (result) {
@@ -131,7 +131,9 @@ export function FilterDetailsStep({
render={({ field }) => (
<FormItem className="flex flex-col">
<div className="flex items-center justify-between text-xs">
<FormLabel>Contract Addresses</FormLabel>
<FormLabel>
Contract Addresses <span className="text-red-500">*</span>
</FormLabel>
<p className="text-muted-foreground">
Enter a contract address
</p>
@@ -196,7 +198,9 @@ export function FilterDetailsStep({
render={({ field }) => (
<FormItem className="flex flex-col">
<div className="flex items-center justify-between text-xs">
<FormLabel>From Address</FormLabel>
<FormLabel>
From Address <span className="text-red-500">*</span>
</FormLabel>
<p className="text-muted-foreground">
Enter a from address
</p>
@@ -29,7 +29,7 @@ export default async function WebhooksLayout(props: {
path: `/team/${params.team_slug}/${params.project_slug}/webhooks`,
},
{
name: "Universal Bridge",
name: "Payments",
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/universal-bridge`,
},
]}
@@ -19,7 +19,7 @@ export default async function Page(props: {
return (
<div>
<h2 className="mb-0.5 font-semibold text-xl tracking-tight">
Universal Bridge Webhooks
Payments Webhooks
</h2>
<p className="text-muted-foreground text-sm">
Get notified for Bridge, Swap and Onramp events.{" "}
@@ -24,75 +24,108 @@ const inputAbi = z.object({
type: z.string(),
});
export const webhookFormSchema = z.object({
abi: z.string().optional(),
addresses: z
.string()
.optional()
.refine(
(val) => {
if (val === undefined || val.trim() === "") {
return true;
}
return val
.split(/[,\s]+/)
.filter(Boolean)
.every((a) => isAddress(a.trim()));
},
{
message: "Enter valid addresses (comma-separated) or leave empty",
},
),
chainIds: z
.array(z.string())
.min(1, { message: "Select at least one chain" }),
eventTypes: z.array(z.string()).optional(),
filterType: z.enum(["event", "transaction"]),
fromAddresses: z
.string()
.optional()
.refine(
(val) => {
if (val === undefined || val.trim() === "") {
return true;
}
return val
.split(/[,\s]+/)
.filter(Boolean)
.every((a) => isAddress(a.trim()));
},
{
message: "Enter valid addresses (comma-separated) or leave empty",
},
),
inputAbi: z.array(inputAbi).optional(),
name: z
.string()
.min(3, { message: "Name must be at least 3 characters long" })
.max(100, { message: "Name must be at most 100 characters long" }),
params: z.record(z.unknown()).optional(),
secret: z.string().optional(),
sigHash: z.string().optional(),
sigHashAbi: z.string().optional(),
toAddresses: z
.string()
.optional()
.refine(
(val) => {
if (val === undefined || val.trim() === "") {
return true;
}
return val
.split(/[,\s]+/)
.filter(Boolean)
.every((a) => isAddress(a.trim()));
},
{
message: "Enter valid addresses (comma-separated) or leave empty",
},
),
webhookUrl: z.string().url({ message: "Must be a valid URL" }),
});
export const webhookFormSchema = z
.object({
abi: z.string().optional(),
addresses: z
.string()
.optional()
.refine(
(val: string | undefined) => {
if (val === undefined || val.trim() === "") {
return true;
}
return val
.split(/[,\s]+/)
.filter(Boolean)
.every((a: string) => isAddress(a.trim()));
},
{
message: "Enter valid addresses (comma-separated)",
},
),
chainIds: z
.array(z.string())
.min(1, { message: "Select at least one chain" }),
eventTypes: z.array(z.string()).optional(),
filterType: z.enum(["event", "transaction"]),
fromAddresses: z
.string()
.optional()
.refine(
(val: string | undefined) => {
if (val === undefined || val.trim() === "") {
return true;
}
return val
.split(/[,\s]+/)
.filter(Boolean)
.every((a: string) => isAddress(a.trim()));
},
{
message: "Enter valid addresses (comma-separated)",
},
),
inputAbi: z.array(inputAbi).optional(),
name: z
.string()
.min(3, { message: "Name must be at least 3 characters long" })
.max(100, { message: "Name must be at most 100 characters long" }),
params: z.record(z.unknown()).optional(),
secret: z.string().optional(),
sigHash: z.string().optional(),
sigHashAbi: z.string().optional(),
toAddresses: z
.string()
.optional()
.refine(
(val: string | undefined) => {
if (val === undefined || val.trim() === "") {
return true;
}
return val
.split(/[,\s]+/)
.filter(Boolean)
.every((a: string) => isAddress(a.trim()));
},
{
message: "Enter valid addresses (comma-separated) or leave empty",
},
),
webhookUrl: z.string().url({ message: "Must be a valid URL" }),
})
.refine(
(data: {
filterType: "event" | "transaction";
addresses?: string;
fromAddresses?: string;
}) => {
if (data.filterType === "event") {
return data.addresses && data.addresses.trim() !== "";
}
return true;
},
{
message: "Contract address is required for event webhooks",
path: ["addresses"],
},
)
.refine(
(data: {
filterType: "event" | "transaction";
addresses?: string;
fromAddresses?: string;
}) => {
if (data.filterType === "transaction") {
return data.fromAddresses && data.fromAddresses.trim() !== "";
}
return true;
},
{
message: "From address is required for transaction webhooks",
path: ["fromAddresses"],
},
);
export type WebhookFormValues = z.infer<typeof webhookFormSchema>;
+1 -1
View File
@@ -37,7 +37,7 @@
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
"zod": "3.25.71"
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
+1 -1
View File
@@ -32,7 +32,7 @@
"tailwind-merge": "^2.6.0",
"thirdweb": "workspace:*",
"use-debounce": "^10.0.5",
"zod": "3.25.71"
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
@@ -25,5 +25,7 @@ export type BridgeComponentsPlaygroundOptions = {
transactionData?: string; // Simplified for demo; could be more complex in real implementation
paymentMethods: ("crypto" | "card")[];
showThirdwebBranding: boolean;
};
};
@@ -464,6 +464,23 @@ export function LeftSection(props: {
}}
theme={options.theme}
/>
<div className="my-4 flex items-center gap-2">
<Checkbox
checked={payOptions.showThirdwebBranding}
id={"branding"}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
showThirdwebBranding: checked === true,
},
}));
}}
/>
<Label htmlFor={"branding"}>Show Branding</Label>
</div>
</CollapsibleSection>
<CollapsibleSection icon={FuelIcon} title="Sponsor gas fees">
@@ -64,6 +64,7 @@ export function RightSection(props: {
theme={themeObj}
title={props.options.payOptions.title}
tokenAddress={props.options.payOptions.buyTokenAddress}
showThirdwebBranding={props.options.payOptions.showThirdwebBranding}
/>
);
}
@@ -87,6 +88,7 @@ export function RightSection(props: {
seller={props.options.payOptions.sellerAddress}
theme={themeObj}
tokenAddress={props.options.payOptions.buyTokenAddress}
showThirdwebBranding={props.options.payOptions.showThirdwebBranding}
/>
);
}
@@ -106,6 +108,7 @@ export function RightSection(props: {
to: account?.address || "",
tokenId: 2n,
})}
showThirdwebBranding={props.options.payOptions.showThirdwebBranding}
/>
);
}
@@ -18,6 +18,7 @@ const defaultConnectOptions: BridgeComponentsPlaygroundOptions = {
title: "",
transactionData: "",
widget: "buy",
showThirdwebBranding: true,
},
theme: {
darkColorOverrides: {},
-53
View File
@@ -5533,60 +5533,7 @@ let returnType: Promise<string>;
The deployed contract address.
---
## DEPLOY.deployPackContract
Deprecated
\[Pack contract is incompatible with Pectra update. Support for this contract is being removed in next release.\]
Deploy a thirdweb Pack contract
### Example
```ts
import { deployPackContract } from "thirdweb/extensions/deploy";
const packAddress = await deployPackContract({
account,
client,
chain,
params: {
name: "Pack contract name",
symbol: "PACK1155",
},
});
```
```ts
function deployPackContract(options: {
account: Account;
chain: Readonly;
client: ThirdwebClient;
params: PackContractParams;
}): Promise<string>;
```
### Parameters
params for deploying [Pack contract](https://thirdweb.com/thirdweb.eth/Pack)
#### Type
```ts
let options: {
account: Account;
chain: Readonly;
client: ThirdwebClient;
params: PackContractParams;
};
```
### Returns
```ts
let returnType: Promise<string>;
```
---
## DEPLOY.deployPublishedContract
+1 -1
View File
@@ -135,7 +135,7 @@
* [DEPLOY.deployERC1155Contract](https://portal.thirdweb.com/references/typescript/v5/deploy/deployERC1155Contract): Deploys an thirdweb ERC1155 contract of the given type. On chains where the thirdweb infrastructure contracts are not deployed, this function will deploy them as well.
* [DEPLOY.deployERC20Contract](https://portal.thirdweb.com/references/typescript/v5/deploy/deployERC20Contract): Deploys an thirdweb ERC20 contract of the given type. On chains where the thirdweb infrastructure contracts are not deployed, this function will deploy them as well.
* [DEPLOY.deployERC721Contract](https://portal.thirdweb.com/references/typescript/v5/deploy/deployERC721Contract): Deploys an thirdweb ERC721 contract of the given type. On chains where the thirdweb infrastructure contracts are not deployed, this function will deploy them as well.
* [DEPLOY.deployPackContract](https://portal.thirdweb.com/references/typescript/v5/deploy/deployPackContract): \[Pack contract is incompatible with Pectra update. Support for this contract is being removed in next release.\]
* [DEPLOY.deployPublishedContract](https://portal.thirdweb.com/references/typescript/v5/deploy/deployPublishedContract): Deploy an instance of a published contract on a given chain
* [DEPLOY.deploySplitContract](https://portal.thirdweb.com/references/typescript/v5/deploy/deploySplitContract): Deploys a thirdweb [Split contract](https://thirdweb.com/thirdweb.eth/Split)On chains where the thirdweb infrastructure contracts are not deployed, this function will deploy them as well.
* [DEPLOY.prepareDeterministicDeployTransaction](https://portal.thirdweb.com/references/typescript/v5/deploy/prepareDeterministicDeployTransaction): Deploy a contract deterministically - will maintain the same address across chains. This is meant to be used with published contracts configured with the 'direct deploy' method. Under the hood, this uses a keyless transaction with a common create2 factory.
+6 -2
View File
@@ -369,6 +369,8 @@ const typescriptRedirects = {
"/typescript/sdk.thirdwebsdk.fromwallet":
"/references/typescript/v4/ThirdwebSDK#fromWallet",
"/typescript/sdk.thirdwebsdk.smartcontract": "/typescript/v4/extensions",
// v5 deploy redirects
"/references/typescript/v5/deploy/deployPackContract": "/contracts",
};
const reactNativeRedirects = {
@@ -803,7 +805,7 @@ const contractRedirects = {
"/contracts/explore/pre-built-contracts/nft-drop",
"/pre-built-contracts/open-edition-erc721":
"/contracts/explore/pre-built-contracts/open-edition",
"/pre-built-contracts/pack": "/contracts/explore/pre-built-contracts/pack",
"/pre-built-contracts/pack": "/contracts",
"/pre-built-contracts/signature-drop": "/contracts",
"/pre-built-contracts/split": "/contracts/explore/pre-built-contracts/split",
"/pre-built-contracts/stake-erc20":
@@ -816,6 +818,8 @@ const contractRedirects = {
"/pre-built-contracts/token-drop":
"/contracts/explore/pre-built-contracts/token-drop",
"/pre-built-contracts/vote": "/contracts/explore/pre-built-contracts/vote",
// pack contract deprecation redirect
"/contracts/explore/pre-built-contracts/pack": "/contracts",
//publish
"/publish": "/contracts/publish/overview",
"/publish/deployment-options": "/contracts/publish/publish-options",
@@ -940,7 +944,7 @@ const otherRedirects = {
// solidity sdk
"/contracts/nft-drop": "/contracts/explore/pre-built-contracts/nft-drop",
"/contracts/nfts-and-tokens": "/contracts",
"/contracts/pack": "/contracts/explore/pre-built-contracts/pack",
"/contracts/pack": "/contracts",
"/contracts/split": "/contracts/explore/pre-built-contracts/split",
"/contracts/token": "/contracts/explore/pre-built-contracts/token",
"/contracts/token-drop": "/contracts/explore/pre-built-contracts/token-drop",
-8
View File
@@ -51,10 +51,6 @@ const links = [
href: "/insight",
name: "Insight",
},
{
href: "/nebula",
name: "Nebula",
},
{
href: "/vault",
name: "Vault",
@@ -127,10 +123,6 @@ export const connectLinks: Array<{
] as const;
const apisLinks = [
{
href: "/nebula/api-reference",
name: "Nebula",
},
{
href: "https://insight.thirdweb.com/reference",
name: "Insight",
+66 -33
View File
@@ -3,6 +3,7 @@ import {
GithubTemplateCard,
Grid,
ExpandableGrid,
FeatureCard,
createMetadata,
OpenSourceCard,
Stack,
@@ -12,7 +13,6 @@ import {
ArticleIconCard,
DocImage
} from "@doc";
import { ExternalLink } from "lucide-react";
import {
TypeScriptIcon,
ReactIcon,
@@ -24,6 +24,8 @@ import { cn } from "@/lib/utils";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import SupportedChains from "../_images/supported-chains.png";
import {FuelIcon,MonitorCheckIcon,WalletCardsIcon,UserLockIcon, ExternalLinkIcon, WalletIcon, PencilIcon, ShieldCheckIcon, PiggyBankIcon, GlobeIcon, ComponentIcon, CodeIcon, ApiIcon, WebhookIcon, IdCardIcon, HandCoinsIcon, MonitorSmartphoneIcon} from "lucide-react";
export const metadata = createMetadata({
title: "thirdweb Connect",
@@ -36,26 +38,79 @@ export const metadata = createMetadata({
# Wallets
Wallets is the complete toolkit for connecting every user to your application. It features customizable onboarding flows, self-custodial in-app wallets, account abstraction, onramps, and a performant API to interact with the blockchain.
Wallets is a complete toolkit for connecting any user to your application. It includes **customizable components** for onboarding, **embedded wallets** , **account abstraction**
for gasless transactions, **server wallets** for developers to carry out transactions, and a secure **non-custodial key management system**.
## Live Playground
You can interact with the React SDK in the [Live Playground](https://playground.thirdweb.com/).
<Stack>
<ArticleIconCard
title="Try the demo"
icon={ExternalLink}
title="Try Wallets"
icon={ExternalLinkIcon}
href="https://playground.thirdweb.com"
description="See the SDK in action on the live playground"
description="Try out the wallet components live in the interactive playground"
/>
</Stack>
## Quick starts
## Features
Get started with Wallets in your preferred language.
<div
className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-2"
>
<FeatureCard
title="Non-Custodial Wallets"
description="Secure non-custodial wallets only accessible by the owner."
iconUrl={<UserLockIcon />}
/>
<FeatureCard
title="EOA Support"
description="Connect to over 500+ EOA wallet providers for crypto native users."
iconUrl={<WalletCardsIcon />}
/>
<FeatureCard
title="Gas Sponsorship"
description="Support for gasless transactions with account abstraction."
iconUrl={<FuelIcon />}
/>
<FeatureCard
title="Ecosystem Wallets"
description="Global wallets to connect your users to any application in an ecosystem."
iconUrl={<MonitorCheckIcon />}
/>
<FeatureCard
title="Cross Platform Support"
description="Create and manage wallets for web, mobile, games, and agents."
iconUrl={<MonitorSmartphoneIcon />}
/>
<FeatureCard
title="One-Click Funding"
description="Buy crypto or swap tokens directly in the wallet modal."
iconUrl={<HandCoinsIcon />}
/>
<FeatureCard
title="Authentication"
description="Enable SIWE standard for blockchain-based ID verification."
iconUrl={<IdCardIcon />}
/>
<FeatureCard
title="Hardware-Level Security"
description="Built with AWS Enclaves (TEE) to isolate and protect keys."
iconUrl={<ShieldCheckIcon />}
/>
</div>
## Quick Starts
<Grid>
<SDKCard
@@ -78,37 +133,15 @@ Get started with Wallets in your preferred language.
/>
</Grid>
## With Wallets, you can
- **Connect to 500+ different wallet providers** with support for every EVM network
- **Log in and authenticate your users** with customizable and secure [email, phone, passkeys and social login](https://portal.thirdweb.com/connect/in-app-wallet/overview) flows.
- **Sponsor transactions** to seamlessly onboard non-native or new crypto users with [Account Abstraction](https://portal.thirdweb.com/connect/account-abstraction/overview).
- **Perform wallet actions** like connecting and disconnecting wallets, viewing balance, displaying ENS names, and execute transactions with our [performant, reliable and type safe API](https://portal.thirdweb.com/connect/blockchain-api)
- **Easily integrate with thirdweb's contracts** to enable users to interact with your application.
- **Facilitate payments** by letting user top up their wallets or do onchain purchases with a credit card with [Pay](https://portal.thirdweb.com/connect/pay/overview).
<Callout variant="info" title="Have you generated a client ID?">
<div className="flex flex-col gap-4 items-start">
<p>
You'll need a client ID to access the Wallet APIs, storage, and more.
</p>
<Button asChild variant='outline'>
<Link href="https://thirdweb.com/create-api-key" target="_blank"> Generate </Link>
</Button>
</div>
</Callout>
## Supported Chains
Wallets are supported on every EVM compatible chain. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist).
Wallets are supported on EVM compatible chains. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist?service=connect-sdk).
<DocImage src={SupportedChains} />
## Starter Kits & demos
View all available starter kits, demos and templates on Github.
View all available starter kits, demos, and templates on Github.
<GithubTemplateCard
title="View all templates"
-4
View File
@@ -17,10 +17,6 @@ export const sidebar: SideBar = {
href: "/connect",
name: "Introduction",
},
{
href: "/connect/why-thirdweb",
name: "Why thirdweb?",
},
{
href: "https://playground.thirdweb.com/",
icon: <ExternalLinkIcon />,
@@ -1,56 +0,0 @@
import { createMetadata } from "@doc";
export const metadata = createMetadata({
image: {
title: "Pack contract",
icon: "contract",
},
title: "Pack contract | thirdweb",
description: `The packs are ERC1155 NFTs themselves, which can have the metadata you'd expect of an NFT, such as a name, image, description, etc.`,
});
# Pack
The packs are ERC1155 NFTs themselves, which can have the metadata you'd expect of an NFT,
such as a name, image, description, etc.
When a pack is opened, a pre-defined quantity of tokens are randomly selected from the ones
that were used to create the packs _(that haven't already been selected)_, and awarded to the opener.
The pack NFT is burned as it's opened.
You can configure how many tokens get selected for the pack:
- How many tokens are in each "unit" (for ERC20 & ERC1155 tokens - one unit could be multiple tokens).
- How many units are in each pack.
> **IMPORTANT**: _Pack functions, such as opening of packs, can be costly in terms of gas usage due to random selection of rewards. Please check your gas estimates/usage, and do a trial on testnets before any mainnet deployment._
## Use Cases & Examples
You could use the Pack contract to:
- Create randomized NFT loot boxes
- Run a randomized raffle where some users open a winning ticket
- Reward your community with token gift boxes
## Smart Contract Design
For a detailed exploration of how the pack smart contract works, why it was built,
and it's limitations, visit our [Pack Design Doc](/contracts/design-docs/pack)!
## Creating & Configuring the Pack
Learn how to create and configure your smart contract using the [dashboard](https://thirdweb.com/team).
### Creating a Pack Contract
Deploy the pack contract to any of our
[supported networks](https://blog.thirdweb.com/guides/which-network-should-you-use) using the button below.
### Contract Metadata
To configure the image, name, and description of the contract, click the **Settings** tab:
### Royalty & Platform Fees
From the **Settings** tab, you also configure the royalty fee.
@@ -0,0 +1,23 @@
import { createMetadata, Details } from "@doc";
export const metadata = createMetadata({
title: "Contracts FAQs",
description: "Frequently asked questions about thirdweb contracts",
image: {
title: "Contracts FAQs",
icon: "contract",
},
});
# Contracts FAQs
<Details summary="Why can I no longer deploy a pack contract?">
Pack contracts were officially deprecated after the Pectra release from Ethereum. The pack contract implementation is incompatible with the Pectra update, which introduced breaking changes to the Ethereum protocol. As a result, support for pack contracts has been removed from thirdweb's deployment tools to ensure compatibility and security.
If you need similar functionality, consider using:
- **Edition Drop contracts** for distributing multiple quantities of NFTs
- **Marketplace contracts** for trading and bundle functionality
- **Custom smart contracts** built with our modular contract framework
For existing pack contracts, they will continue to function on-chain, but new deployments are no longer supported.
</Details>
+4 -4
View File
@@ -212,10 +212,6 @@ export const sidebar: SideBar = {
href: `${prebuiltSlug}/multiwrap`,
name: "Multiwrap",
},
{
href: `${prebuiltSlug}/pack`,
name: "Pack",
},
{
href: `${prebuiltSlug}/split`,
name: "Split",
@@ -810,6 +806,10 @@ export const sidebar: SideBar = {
{
isCollapsible: false,
links: [
{
href: "/contracts/faq",
name: "FAQs",
},
{
links: [
{
@@ -0,0 +1,330 @@
# Session Keys Guide
Session keys enable secure transaction execution on behalf of smart accounts without requiring direct access to the main account's private key. This guide will walk you through creating and using session keys with the thirdweb TypeScript SDK.
## Prerequisites
Before you begin, ensure you have:
- A thirdweb client configured
- Access to a session key account address
- Vault access token for Engine operations
## Setup
First, let's set up the necessary imports and configuration:
```typescript
import {
generateAccount,
smartWallet,
sendTransaction,
getContract,
createThirdwebClient
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { getAllActiveSigners } from "thirdweb/extensions/erc4337";
import { Engine } from "thirdweb/engine";
// Configure your client
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key" // Only use in server environments
});
// Your session key account address
const sessionKeyAccountAddress = "0x..."; // Replace with your session key address
// Target address for transactions
const targetAddress = "0x..."; // Replace with your target address
```
## Step 1: Configure User Smart Wallet with Session Key
The first step is to add our session key address as a signer to the user's smart account. This is typically done on the client side since it needs explicit user approval. This can be done by configuring the smart wallet with the session key address and permissions.
In a React application, this can be done by using the `ConnectButton` or `ConnectEmbed` component. This will automatically configure the smart wallet with the session key address and permissions.
```typescript
<ConnectButton
accountAbstraction={{
chain: sepolia,
sessionKey: {
address: sessionKeyAccountAddress,
permissions: {
// "*" allows all targets, or specify specific contract addresses
approvedTargets: "*",
},
},
sponsorGas: true,
}}
/>
```
This can also be done in pure TypeScript by using the `smartWallet` function and connecting it to a personal account.
For this guide, we'll generate a random personal account that will be used to create the smart wallet:
```typescript
// this would be the user's personal account
const personalAccount = await generateAccount({
client: client,
});
// wrapped in a smart wallet with session key permissions
const smart = smartWallet({
chain: sepolia,
sessionKey: {
address: sessionKeyAccountAddress,
permissions: {
// "*" allows all targets, or specify specific contract addresses
approvedTargets: "*",
},
},
sponsorGas: true, // Enable gas sponsorship
});
console.log("Personal account created:", personalAccount.address);
```
### Session Key Permissions
The `permissions` object allows you to control what the session key can do:
- `approvedTargets`: Specify which contract addresses the session key can interact with
- Use `"*"` for all targets
- Use an array of addresses for specific contracts: `["0x123...", "0x456..."]`
## Step 2: Connect Smart Account
Connect the smart wallet using the personal account:
```typescript
const smartAccount = await smart.connect({
client: client,
personalAccount: personalAccount,
});
console.log("Smart account address:", smartAccount.address);
```
Note that in a React application, this would be done automatically by the `ConnectButton` or `ConnectEmbed` component.
## Step 3 (Optional): Verify Session Key Registration
Check that the session key is properly registered as an active signer:
```typescript
const signers = await getAllActiveSigners({
contract: getContract({
address: smartAccount.address,
chain: sepolia,
client: client,
}),
});
// Verify the session key is in the list of active signers
const isSessionKeyActive = signers
.map((s) => s.signer)
.includes(sessionKeyAccountAddress);
console.log("Session key is active:", isSessionKeyActive);
console.log("All active signers:", signers.map((s) => s.signer));
```
## Step 4: Create Engine Server Wallet
Set up an Engine server wallet using the session key for transaction execution:
```typescript
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client: client,
executionOptions: {
entrypointVersion: "0.6", // ERC-4337 entrypoint version
signerAddress: sessionKeyAccountAddress,
smartAccountAddress: smartAccount.address,
type: "ERC4337",
},
vaultAccessToken: process.env.VAULT_TOKEN as string, // Your vault access token
});
```
### Execution Options
- `entrypointVersion`: The ERC-4337 entrypoint version to use
- `signerAddress`: The session key address that will sign transactions
- `smartAccountAddress`: The smart account address that will execute transactions
- `type`: The account abstraction type (ERC4337)
## Step 5: Execute Transactions
Now you can execute transactions using the session key:
```typescript
const tx = await sendTransaction({
account: serverWallet,
transaction: {
chain: sepolia,
client: client,
to: targetAddress,
value: 0n, // Amount in wei (0 for no ETH transfer)
// data: "0x...", // Optional: contract call data
},
});
console.log("Transaction sent:", tx.transactionHash);
```
## Complete Example
Here's a complete example putting it all together:
```typescript
import {
generateAccount,
smartWallet,
sendTransaction,
getContract,
createThirdwebClient
} from "thirdweb";
import { sepolia } from "thirdweb/chains";
import { getAllActiveSigners } from "thirdweb/extensions/erc4337";
import { Engine } from "thirdweb/engine";
async function executeTransactionWithSessionKey() {
// Configuration
const client = createThirdwebClient({
clientId: "your-client-id",
secretKey: "your-secret-key"
});
const sessionKeyAccountAddress = "0x..."; // Your session key address
const targetAddress = "0x..."; // Target address for the final transaction
try {
// Step 1: Create personal account
const personalAccount = await generateAccount({ client });
// Step 2: Configure smart wallet
const smart = smartWallet({
chain: sepolia,
sessionKey: {
address: sessionKeyAccountAddress,
permissions: {
approvedTargets: "*",
},
},
sponsorGas: true,
});
// Step 3: Connect smart account
const smartAccount = await smart.connect({
client,
personalAccount,
});
// Step 4: Verify session key
const signers = await getAllActiveSigners({
contract: getContract({
address: smartAccount.address,
chain: sepolia,
client,
}),
});
const isSessionKeyActive = signers
.map((s) => s.signer)
.includes(sessionKeyAccountAddress);
if (!isSessionKeyActive) {
throw new Error("Session key is not active");
}
// Step 5: Create server wallet
const serverWallet = Engine.serverWallet({
address: sessionKeyAccountAddress,
chain: sepolia,
client,
executionOptions: {
entrypointVersion: "0.6",
signerAddress: sessionKeyAccountAddress,
smartAccountAddress: smartAccount.address,
type: "ERC4337",
},
vaultAccessToken: process.env.VAULT_TOKEN as string,
});
// Step 6: Execute transaction
const tx = await sendTransaction({
account: serverWallet,
transaction: {
chain: sepolia,
client,
to: targetAddress,
value: 0n,
},
});
console.log("Transaction successful:", tx.transactionHash);
return tx;
} catch (error) {
console.error("Error executing transaction:", error);
throw error;
}
}
// Execute the function
executeTransactionWithSessionKey()
.then((tx) => console.log("Done!", tx.transactionHash))
.catch((error) => console.error("Failed:", error));
```
## Security Considerations
- **Session Key Storage**: Store session keys securely, preferably in a vault system
- **Permission Scope**: Limit session key permissions to only necessary targets
- **Key Rotation**: Regularly rotate session keys for enhanced security
- **Monitoring**: Monitor session key usage for suspicious activity
## Troubleshooting
### Common Issues
1. **Session key not active**: Ensure the session key is properly registered with the smart account
2. **Permission denied**: Check that the target address is included in `approvedTargets`
3. **Gas estimation failed**: Verify that gas sponsorship is properly configured
4. **Vault token invalid**: Ensure your vault access token is valid and has proper permissions
### Error Handling
Always wrap your session key operations in try-catch blocks:
```typescript
try {
const tx = await sendTransaction({
account: serverWallet,
transaction: {
chain: sepolia,
client,
to: targetAddress,
value: 0n,
},
});
} catch (error) {
if (error.message.includes("permission")) {
console.error("Session key lacks permission for this operation");
} else if (error.message.includes("gas")) {
console.error("Gas estimation or sponsorship failed");
} else {
console.error("Transaction failed:", error);
}
}
```
## Next Steps
- Learn more about [Smart Wallets](/engine/v3/configure-wallets/server-wallets)
- Explore [Engine API Reference](https://engine.thirdweb.com/reference)
- Check out the [TypeScript SDK](/references/typescript/v5/serverWallet) documentation
+11
View File
@@ -1,4 +1,5 @@
import {
BookOpenIcon,
BracesIcon,
CloudIcon,
CodeIcon,
@@ -51,6 +52,16 @@ export const sidebar: SideBar = {
],
name: "Configure Wallets",
},
{
icon: <BookOpenIcon />,
links: [
{
href: `${engineV3Slug}/guides/session-keys`,
name: "Session Keys",
},
],
name: "Guides",
},
{
href: "https://engine.thirdweb.com/reference",
icon: <BracesIcon />,
+1 -14
View File
@@ -14,7 +14,6 @@ import { BridgeIcon } from "../icons/products/BridgeIcon";
import { ConnectIcon } from "../icons/products/ConnectIcon";
import { EngineIcon } from "../icons/products/EngineIcon";
import { InsightIcon } from "../icons/products/InsightIcon";
import { NebulaIcon } from "../icons/products/NebulaIcon";
import { PlaygroundIcon } from "../icons/products/PlaygroundIcon";
import { cn } from "../lib/utils";
import DocsHeroDark from "./_images/docs-hero-dark.png";
@@ -148,12 +147,6 @@ function ReferenceSection() {
iconClassName="text-muted-foreground"
title="Bundler"
/>
<SDKCard
href="/nebula/api-reference"
icon={NebulaIcon} // TODO: actual openAPI docs
iconClassName="text-muted-foreground"
title="Nebula"
/>
</Grid>
</section>
);
@@ -194,16 +187,10 @@ function LearningResourcesSection() {
icon={InsightIcon}
title="Insight"
/>
<ArticleCardIndex
description="API interface for LLMs"
href="/nebula"
icon={NebulaIcon}
title="Nebula"
/>
<ArticleCardIndex
description="Non-custodial key management service"
href="/vault"
icon={NebulaIcon}
icon={ConnectIcon}
title="Vault"
/>
</Grid>
+9 -4
View File
@@ -1,7 +1,7 @@
import { createMetadata, DocImage, Grid, SDKCard, FeatureCard } from "@doc";
import { createMetadata, DocImage, Grid, SDKCard, FeatureCard, ArticleIconCard } from "@doc";
import PayOverviewImage from "./assets/pay-overview.png";
import SupportedChains from "../_images/supported-chains.png";
import {RocketIcon, ArrowLeftRightIcon, WalletIcon, PencilIcon, ShieldCheckIcon, PiggyBankIcon, GlobeIcon, ComponentIcon, CodeIcon, ApiIcon, WebhookIcon} from "lucide-react";
import {ExternalLinkIcon, RocketIcon, ArrowLeftRightIcon, WalletIcon, PencilIcon, ShieldCheckIcon, PiggyBankIcon, GlobeIcon, ComponentIcon, CodeIcon, ApiIcon, WebhookIcon} from "lucide-react";
export const metadata = createMetadata({
image: {
@@ -17,9 +17,14 @@ export const metadata = createMetadata({
Payments allows you to create both simple and advanced payment flows for bridging, swapping, onramping, and peer-to-peer purchases. It's been used to drive millions in NFT sales, bridge native tokens to brand new chains, send stablecoins between users, and more.
To get started check out the [SDK functions](https://portal.thirdweb.com/typescript/v5/buy/quote), [API reference](https://bridge.thirdweb.com/reference), or [playground](https://playground.thirdweb.com/connect/pay).
## Live Playground
<DocImage src={PayOverviewImage} />
<ArticleIconCard
title="Try Payments"
icon={ExternalLinkIcon}
href="https://playground.thirdweb.com/connect/pay"
description="Try out the payment components live in the interactive playground"
/>
## Features
+1 -1
View File
@@ -21,7 +21,7 @@
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
"tiny-invariant": "^1.3.3",
"zod": "3.25.71"
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
+2 -2
View File
@@ -4,8 +4,8 @@
"actions": {
"source": {
"organizeImports": "on",
"useSortedAttributes": "on",
"useSortedKeys": "on"
"useSortedAttributes": "off",
"useSortedKeys": "off"
}
},
"enabled": true
+28
View File
@@ -1,5 +1,33 @@
# @thirdweb-dev/nebula
## 0.2.12
### Patch Changes
- Updated dependencies [[`bfd728a`](https://github.com/thirdweb-dev/js/commit/bfd728a9d1aab7d0044791b218abf9ac9dbc9bf6)]:
- thirdweb@5.105.12
## 0.2.11
### Patch Changes
- Updated dependencies [[`692cedf`](https://github.com/thirdweb-dev/js/commit/692cedf9211fb5a6c24406df69155b920f7d2557)]:
- thirdweb@5.105.11
## 0.2.10
### Patch Changes
- Updated dependencies [[`415ad90`](https://github.com/thirdweb-dev/js/commit/415ad909865d9be942771fb428f904e17916127d), [`c644463`](https://github.com/thirdweb-dev/js/commit/c64446306e7ee41110ca3f5bc6994a69d3f4de20), [`10c3b6f`](https://github.com/thirdweb-dev/js/commit/10c3b6f27fe4b73f5b1c71c4395ca350e5cd26d9)]:
- thirdweb@5.105.10
## 0.2.9
### Patch Changes
- Updated dependencies [[`f48d800`](https://github.com/thirdweb-dev/js/commit/f48d8006bc5845c05f9bfc050a850edfaee7bdb5)]:
- thirdweb@5.105.9
## 0.2.8
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.8"
"version": "0.2.12"
}
+1 -1
View File
@@ -7,7 +7,7 @@
"@confluentinc/kafka-javascript": "1.4.0",
"@paralleldrive/cuid2": "^2.2.2",
"aws4fetch": "1.0.20",
"zod": "3.25.71"
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
+28
View File
@@ -1,5 +1,33 @@
# thirdweb
## 5.105.12
### Patch Changes
- [#7575](https://github.com/thirdweb-dev/js/pull/7575) [`bfd728a`](https://github.com/thirdweb-dev/js/commit/bfd728a9d1aab7d0044791b218abf9ac9dbc9bf6) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Adds the ability to hide thirdweb branding in the payment widgets with showThirdwebBranding
## 5.105.11
### Patch Changes
- [#7567](https://github.com/thirdweb-dev/js/pull/7567) [`692cedf`](https://github.com/thirdweb-dev/js/commit/692cedf9211fb5a6c24406df69155b920f7d2557) Thanks [@MananTank](https://github.com/MananTank)! - Restore CSS resets on react components
## 5.105.10
### Patch Changes
- [#7540](https://github.com/thirdweb-dev/js/pull/7540) [`415ad90`](https://github.com/thirdweb-dev/js/commit/415ad909865d9be942771fb428f904e17916127d) Thanks [@Yash094](https://github.com/Yash094)! - add trextestnet to chains package
- [#7551](https://github.com/thirdweb-dev/js/pull/7551) [`c644463`](https://github.com/thirdweb-dev/js/commit/c64446306e7ee41110ca3f5bc6994a69d3f4de20) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Use new BuyWidget in wallet details modal
- [#7556](https://github.com/thirdweb-dev/js/pull/7556) [`10c3b6f`](https://github.com/thirdweb-dev/js/commit/10c3b6f27fe4b73f5b1c71c4395ca350e5cd26d9) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Respect fee payer in token payment selection quotes
## 5.105.9
### Patch Changes
- [#7536](https://github.com/thirdweb-dev/js/pull/7536) [`f48d800`](https://github.com/thirdweb-dev/js/commit/f48d8006bc5845c05f9bfc050a850edfaee7bdb5) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Optimize fetching payment tokens in payment widgets
## 5.105.8
### Patch Changes
+2 -2
View File
@@ -39,7 +39,7 @@
"toml": "3.0.0",
"uqr": "0.1.2",
"viem": "2.28.1",
"zod": "3.25.71"
"zod": "3.25.75"
},
"devDependencies": {
"@aws-sdk/client-kms": "3.592.0",
@@ -404,5 +404,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.105.8"
"version": "5.105.12"
}
+23 -14
View File
@@ -1,6 +1,7 @@
import type { ThirdwebClient } from "../client/client.js";
import { getThirdwebBaseUrl } from "../utils/domains.js";
import { getClientFetch } from "../utils/fetch.js";
import { withCache } from "../utils/promise/withCache.js";
import type { Chain } from "./types/Chain.js";
import { ApiError } from "./types/Errors.js";
@@ -54,22 +55,30 @@ import { ApiError } from "./types/Errors.js";
export async function chains(options: chains.Options): Promise<chains.Result> {
const { client } = options;
const clientFetch = getClientFetch(client);
const url = new URL(`${getThirdwebBaseUrl("bridge")}/v1/chains`);
return withCache(
async () => {
const clientFetch = getClientFetch(client);
const url = new URL(`${getThirdwebBaseUrl("bridge")}/v1/chains`);
const response = await clientFetch(url.toString());
if (!response.ok) {
const errorJson = await response.json();
throw new ApiError({
code: errorJson.code || "UNKNOWN_ERROR",
correlationId: errorJson.correlationId || undefined,
message: errorJson.message || response.statusText,
statusCode: response.status,
});
}
const response = await clientFetch(url.toString());
if (!response.ok) {
const errorJson = await response.json();
throw new ApiError({
code: errorJson.code || "UNKNOWN_ERROR",
correlationId: errorJson.correlationId || undefined,
message: errorJson.message || response.statusText,
statusCode: response.status,
});
}
const { data }: { data: Chain[] } = await response.json();
return data;
const { data }: { data: Chain[] } = await response.json();
return data;
},
{
cacheKey: "bridge-chains",
cacheTime: 1000 * 60 * 60 * 1, // 1 hours
},
);
}
export declare namespace chains {
@@ -0,0 +1,17 @@
import { defineChain } from "../utils.js";
/**
* @chain
*/
export const tRexTestnet = /*@__PURE__*/ defineChain({
blockExplorers: [
{
name: "T-Rex Testnet Block Explorer",
url: "https://testnet.trex.xyz/",
},
],
id: 1962,
name: "T-Rex Testnet",
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
testnet: true,
});
-8
View File
@@ -88,14 +88,6 @@ export type ChainMetadata = {
stackType: string;
};
/**
* @chain
*/
export type ChainService = {
service: string;
enabled: boolean;
};
/**
* @chain
*/
+10 -51
View File
@@ -7,7 +7,6 @@ import type {
Chain,
ChainMetadata,
ChainOptions,
ChainService,
LegacyChain,
} from "./types.js";
@@ -323,62 +322,22 @@ export function getChainMetadata(chain: Chain): Promise<ChainMetadata> {
);
}
type FetchChainServiceResponse =
| {
data: {
services: ChainService[];
};
error?: never;
}
| {
data?: never;
error: unknown;
};
/**
* Retrieves a list of services available on a given chain
* @param chain - The chain object containing the chain ID.
* @returns A Promise that resolves to chain services.
* @throws If there is an error fetching the chain services.
* @example
* ```ts
* const chain = defineChain({ id: 1 });
* const chainServices = await getChainServices(chain);
* console.log(chainServices);
* ```
* @chain
*/
export function getChainServices(chain: Chain): Promise<ChainService[]> {
const chainId = chain.id;
export async function getInsightEnabledChainIds(): Promise<number[]> {
return withCache(
async () => {
try {
const res = await fetch(
`https://api.thirdweb.com/v1/chains/${chainId}/services`,
const res = await fetch(
`https://api.thirdweb.com/v1/chains/services?service=insight`,
);
if (!res.ok) {
throw new Error(
`Failed to fetch services. ${res.status} ${res.statusText}`,
);
if (!res.ok) {
throw new Error(
`Failed to fetch services for chainId ${chainId}. ${res.status} ${res.statusText}`,
);
}
const response = (await res.json()) as FetchChainServiceResponse;
if (response.error) {
throw new Error(`Failed to fetch services for chainId ${chainId}`);
}
if (!response.data) {
throw new Error(`Failed to fetch services for chainId ${chainId}`);
}
const services = response.data.services;
return services;
} catch {
throw new Error(`Failed to fetch services for chainId ${chainId}`);
}
const response = (await res.json()) as { data: Record<number, boolean> };
return Object.keys(response.data).map((chainId) => Number(chainId));
},
{
cacheKey: `chain:${chainId}:services`,
cacheKey: `chain:insight-enabled`,
cacheTime: 24 * 60 * 60 * 1000, // 1 day
},
);
+1
View File
@@ -72,6 +72,7 @@ export { scrollSepoliaTestnet } from "../chains/chain-definitions/scroll-sepolia
export { sepolia } from "../chains/chain-definitions/sepolia.js";
export { somniaTestnet } from "../chains/chain-definitions/somniaTestnet.js";
export { soneiumMinato } from "../chains/chain-definitions/soneium-minato.js";
export { tRexTestnet } from "../chains/chain-definitions/t-rex-testnet.js";
export { treasure } from "../chains/chain-definitions/treasure.js";
export { treasureTopaz } from "../chains/chain-definitions/treasureTopaz.js";
export { xai } from "../chains/chain-definitions/xai.js";
+6 -19
View File
@@ -1,29 +1,16 @@
import type { Chain } from "../chains/types.js";
import { getChainServices } from "../chains/utils.js";
import { getInsightEnabledChainIds } from "../chains/utils.js";
export async function assertInsightEnabled(chains: Chain[]) {
const chainData = await Promise.all(
chains.map((chain) =>
isInsightEnabled(chain).then((enabled) => ({
chain,
enabled,
})),
),
);
const insightEnabled = chainData.every((c) => c.enabled);
const chainIds = await getInsightEnabledChainIds();
const insightEnabled = chains.every((c) => chainIds.includes(c.id));
if (!insightEnabled) {
throw new Error(
`Insight is not available for chains ${chainData
.filter((c) => !c.enabled)
.map((c) => c.chain.id)
`Insight is not available for chains ${chains
.filter((c) => !chainIds.includes(c.id))
.map((c) => c.id)
.join(", ")}`,
);
}
}
export async function isInsightEnabled(chain: Chain) {
const chainData = await getChainServices(chain);
return chainData.some((c) => c.service === "insight" && c.enabled);
}
@@ -84,11 +84,6 @@ export type BuyWithCryptoStatus =
purchaseData?: PurchaseData;
};
export type ValidBuyWithCryptoStatus = Exclude<
BuyWithCryptoStatus,
{ status: "NOT_FOUND" }
>;
/**
* Gets the status of a buy with crypto transaction
* @param buyWithCryptoTransaction - Object of type [`BuyWithCryptoTransaction`](https://portal.thirdweb.com/references/typescript/v5/BuyWithCryptoTransaction)
@@ -1,5 +1,4 @@
import { getAddress } from "../../utils/address.js";
import type { PayTokenInfo } from "../utils/commonTypes.js";
import type { BuyWithFiatQuote } from "./getQuote.js";
/**
@@ -28,58 +27,3 @@ export function isSwapRequiredPostOnramp(
return !(sameChain && sameToken);
}
export function getOnRampSteps(
buyWithFiatQuote: BuyWithFiatQuote,
): OnRampStep[] {
const isSwapRequired = isSwapRequiredPostOnramp(buyWithFiatQuote);
if (!isSwapRequired) {
return [
{
action: "buy",
amount: buyWithFiatQuote.estimatedToAmountMin,
token: buyWithFiatQuote.toToken,
},
];
}
if (buyWithFiatQuote.routingToken) {
return [
{
action: "buy",
amount: buyWithFiatQuote.onRampToken.amount,
token: buyWithFiatQuote.onRampToken.token,
},
{
action: "swap",
amount: buyWithFiatQuote.routingToken.amount,
token: buyWithFiatQuote.routingToken.token,
},
{
action: "bridge",
amount: buyWithFiatQuote.estimatedToAmountMin,
token: buyWithFiatQuote.toToken,
},
];
}
return [
{
action: "buy",
amount: buyWithFiatQuote.onRampToken.amount,
token: buyWithFiatQuote.onRampToken.token,
},
{
action: "swap",
amount: buyWithFiatQuote.estimatedToAmountMin,
token: buyWithFiatQuote.toToken,
},
];
}
export type OnRampStep = {
action: "buy" | "swap" | "bridge";
token: PayTokenInfo;
amount: string;
};
@@ -19,4 +19,4 @@ export type PayOnChainTransactionDetails = {
export type FiatProvider = (typeof FiatProviders)[number];
export const FiatProviders = ["coinbase", "stripe", "transak"] as const;
const FiatProviders = ["coinbase", "stripe", "transak"] as const;
@@ -12,12 +12,14 @@ interface UseBridgeQuoteParams {
destinationAmount: bigint;
client: ThirdwebClient;
enabled?: boolean;
feePayer?: "sender" | "receiver";
}
export function useBridgeQuote({
originToken,
destinationToken,
destinationAmount,
feePayer,
client,
enabled = true,
}: UseBridgeQuoteParams) {
@@ -35,6 +37,7 @@ export function useBridgeQuote({
amount: destinationAmount,
chainId: originToken.chainId,
client,
feePayer,
receiver: destinationToken.address,
sender: originToken.address,
tokenAddress: originToken.address,
@@ -59,6 +62,7 @@ export function useBridgeQuote({
destinationToken.chainId,
destinationToken.address,
destinationAmount.toString(),
feePayer,
],
refetchInterval: 60000, // 30 seconds
retry: 3, // 1 minute
@@ -1,9 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { chains } from "../../../bridge/Chains.js";
import { routes } from "../../../bridge/Routes.js";
import type { Token } from "../../../bridge/types/Token.js";
import { getCachedChain } from "../../../chains/utils.js";
import {
getCachedChain,
getInsightEnabledChainIds,
} from "../../../chains/utils.js";
import type { ThirdwebClient } from "../../../client/client.js";
import { isInsightEnabled } from "../../../insight/common.js";
import { getOwnedTokens } from "../../../insight/get-tokens.js";
import { toTokens } from "../../../utils/units.js";
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
@@ -56,42 +59,29 @@ export function usePaymentMethods(options: {
if (!wallet) {
throw new Error("No wallet connected");
}
const allRoutes = await routes({
client,
destinationChainId: destinationToken.chainId,
destinationTokenAddress: destinationToken.address,
includePrices: true,
limit: 100,
maxSteps: 3,
sortBy: "popularity", // Get top 100 most popular routes
});
const allOriginTokens = includeDestinationToken
? [destinationToken, ...allRoutes.map((route) => route.originToken)]
: allRoutes.map((route) => route.originToken);
// 1. Get all supported chains
const [allChains, insightEnabledChainIds] = await Promise.all([
chains({ client }),
getInsightEnabledChainIds(),
]);
// 1. Resolve all unique chains in the supported token map
const uniqueChains = Array.from(
new Set(allOriginTokens.map((t) => t.chainId)),
// 2. Check insight availability for all chains
const insightEnabledChains = allChains.filter((c) =>
insightEnabledChainIds.includes(c.chainId),
);
// 2. Check insight availability once per chain
const insightSupport = await Promise.all(
uniqueChains.map(async (c) => ({
chain: getCachedChain(c),
enabled: await isInsightEnabled(getCachedChain(c)),
})),
);
const insightEnabledChains = insightSupport.filter((c) => c.enabled);
// 3. ERC-20 balances for insight-enabled chains (batched 5 chains / call)
let owned: OwnedTokenWithQuote[] = [];
// 3. Get all owned tokens for insight-enabled chains
let allOwnedTokens: Array<{
balance: bigint;
originToken: Token;
}> = [];
let page = 0;
const limit = 100;
const limit = 500;
while (true) {
const batch = await getOwnedTokens({
chains: insightEnabledChains.map((c) => c.chain),
chains: insightEnabledChains.map((c) => getCachedChain(c.chainId)),
client,
ownerAddress: wallet.getAccount()?.address || "",
queryOptions: {
@@ -105,25 +95,92 @@ export function usePaymentMethods(options: {
break;
}
// find matching origin token in allRoutes
// Convert to our format and filter out zero balances
const tokensWithBalance = batch
.filter((b) => b.value > 0n)
.map((b) => ({
balance: b.value,
originAmount: 0n,
originToken: allOriginTokens.find(
(t) =>
t.address.toLowerCase() === b.tokenAddress.toLowerCase() &&
t.chainId === b.chainId,
),
}))
.filter((t) => !!t.originToken) as OwnedTokenWithQuote[];
originToken: {
address: b.tokenAddress,
chainId: b.chainId,
decimals: b.decimals,
iconUri: "",
name: b.name,
priceUsd: 0,
symbol: b.symbol,
} as Token,
}));
owned = [...owned, ...tokensWithBalance];
allOwnedTokens = [...allOwnedTokens, ...tokensWithBalance];
page += 1;
}
// sort by dollar balance descending
owned.sort((a, b) => {
// 4. For each chain where we have owned tokens, fetch possible routes
const chainsWithOwnedTokens = Array.from(
new Set(allOwnedTokens.map((t) => t.originToken.chainId)),
);
const allValidOriginTokens = new Map<string, Token>();
// Add destination token if included
if (includeDestinationToken) {
const tokenKey = `${destinationToken.chainId}-${destinationToken.address.toLowerCase()}`;
allValidOriginTokens.set(tokenKey, destinationToken);
}
// Fetch routes for each chain with owned tokens
await Promise.all(
chainsWithOwnedTokens.map(async (chainId) => {
try {
// TODO (bridge): this is quite inefficient, need to fix the popularity sorting to really capture all users tokens
const routesForChain = await routes({
client,
destinationChainId: destinationToken.chainId,
destinationTokenAddress: destinationToken.address,
includePrices: true,
limit: 100,
maxSteps: 3,
originChainId: chainId,
});
// Add all origin tokens from this chain's routes
for (const route of routesForChain) {
// Skip if the origin token is the same as the destination token, will be added later only if includeDestinationToken is true
if (
route.originToken.chainId === destinationToken.chainId &&
route.originToken.address.toLowerCase() ===
destinationToken.address.toLowerCase()
) {
continue;
}
const tokenKey = `${route.originToken.chainId}-${route.originToken.address.toLowerCase()}`;
allValidOriginTokens.set(tokenKey, route.originToken);
}
} catch (error) {
// Log error but don't fail the entire operation
console.warn(`Failed to fetch routes for chain ${chainId}:`, error);
}
}),
);
// 5. Filter owned tokens to only include valid origin tokens
const validOwnedTokens: OwnedTokenWithQuote[] = [];
for (const ownedToken of allOwnedTokens) {
const tokenKey = `${ownedToken.originToken.chainId}-${ownedToken.originToken.address.toLowerCase()}`;
const validOriginToken = allValidOriginTokens.get(tokenKey);
if (validOriginToken) {
validOwnedTokens.push({
balance: ownedToken.balance,
originAmount: 0n,
originToken: validOriginToken, // Use the token with pricing info from routes
});
}
}
// Sort by dollar balance descending
validOwnedTokens.sort((a, b) => {
const aDollarBalance =
Number.parseFloat(toTokens(a.balance, a.originToken.decimals)) *
a.originToken.priceUsd;
@@ -135,29 +192,19 @@ export function usePaymentMethods(options: {
const suitableOriginTokens: OwnedTokenWithQuote[] = [];
for (const b of owned) {
if (b.originToken && b.balance > 0n) {
if (
includeDestinationToken &&
b.originToken.address.toLowerCase() ===
destinationToken.address.toLowerCase() &&
b.originToken.chainId === destinationToken.chainId
) {
// add same token to the front of the list
suitableOriginTokens.unshift({
balance: b.balance,
originAmount: 0n,
originToken: b.originToken,
});
continue;
}
suitableOriginTokens.push({
balance: b.balance,
originAmount: 0n,
originToken: b.originToken,
});
for (const token of validOwnedTokens) {
if (
includeDestinationToken &&
token.originToken.address.toLowerCase() ===
destinationToken.address.toLowerCase() &&
token.originToken.chainId === destinationToken.chainId
) {
// Add same token to the front of the list
suitableOriginTokens.unshift(token);
continue;
}
suitableOriginTokens.push(token);
}
const transformedRoutes = [
@@ -2,7 +2,6 @@ import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js";
import type { AuthArgsType } from "../../../wallets/in-app/core/authentication/types.js";
export const LAST_AUTH_PROVIDER_STORAGE_KEY = "lastAuthProvider";
export const PREFERRED_FIAT_PROVIDER_STORAGE_KEY = "preferredFiatProvider";
export async function setLastAuthProvider(
authProvider: AuthArgsType["strategy"],
@@ -120,6 +120,12 @@ export interface BridgeOrchestratorProps {
* @default ["crypto", "card"]
*/
paymentMethods?: ("crypto" | "card")[];
/**
* Whether to show thirdweb branding in the widget.
* @default true
*/
showThirdwebBranding?: boolean;
}
export function BridgeOrchestrator({
@@ -135,6 +141,7 @@ export function BridgeOrchestrator({
paymentLinkId,
presetOptions,
paymentMethods = ["crypto", "card"],
showThirdwebBranding = true,
}: BridgeOrchestratorProps) {
// Initialize adapters
const adapters = useMemo(
@@ -145,6 +152,18 @@ export function BridgeOrchestrator({
[],
);
// Create modified connect options with branding setting
const modifiedConnectOptions = useMemo(() => {
if (!connectOptions) return undefined;
return {
...connectOptions,
connectModal: {
...connectOptions.connectModal,
showThirdwebBranding,
},
};
}, [connectOptions, showThirdwebBranding]);
// Use the payment machine hook
const [state, send] = usePaymentMachine(adapters, uiOptions.mode);
@@ -227,7 +246,10 @@ export function BridgeOrchestrator({
<ErrorBanner
client={client}
error={state.context.currentError}
onCancel={onCancel}
onCancel={() => {
send({ type: "RESET" });
onCancel?.();
}}
onRetry={handleRetry}
/>
)}
@@ -236,10 +258,11 @@ export function BridgeOrchestrator({
{state.value === "init" && uiOptions.mode === "fund_wallet" && (
<FundWallet
client={client}
connectOptions={connectOptions}
connectOptions={modifiedConnectOptions}
onContinue={handleRequirementsResolved}
presetOptions={presetOptions}
receiverAddress={receiverAddress}
showThirdwebBranding={showThirdwebBranding}
uiOptions={uiOptions}
/>
)}
@@ -247,8 +270,9 @@ export function BridgeOrchestrator({
{state.value === "init" && uiOptions.mode === "direct_payment" && (
<DirectPayment
client={client}
connectOptions={connectOptions}
connectOptions={modifiedConnectOptions}
onContinue={handleRequirementsResolved}
showThirdwebBranding={showThirdwebBranding}
uiOptions={uiOptions}
/>
)}
@@ -256,8 +280,9 @@ export function BridgeOrchestrator({
{state.value === "init" && uiOptions.mode === "transaction" && (
<TransactionPayment
client={client}
connectOptions={connectOptions}
connectOptions={modifiedConnectOptions}
onContinue={handleRequirementsResolved}
showThirdwebBranding={showThirdwebBranding}
uiOptions={uiOptions}
/>
)}
@@ -269,9 +294,14 @@ export function BridgeOrchestrator({
<PaymentSelection
client={client}
connectLocale={connectLocale || en}
connectOptions={connectOptions}
connectOptions={modifiedConnectOptions}
destinationAmount={state.context.destinationAmount}
destinationToken={state.context.destinationToken}
feePayer={
uiOptions.mode === "direct_payment"
? uiOptions.paymentInfo.feePayer
: undefined
}
includeDestinationToken={uiOptions.mode !== "fund_wallet"}
onBack={() => {
send({ type: "BACK" });
@@ -102,6 +102,12 @@ export type BuyWidgetProps = {
className?: string;
/**
* Whether to show thirdweb branding in the widget.
* @default true
*/
showThirdwebBranding?: boolean;
/**
* The chain the accepted token is on.
*/
@@ -391,6 +397,7 @@ export function BuyWidget(props: BuyWidgetProps) {
purchaseData={props.purchaseData}
receiverAddress={undefined}
uiOptions={bridgeDataQuery.data.data}
showThirdwebBranding={props.showThirdwebBranding}
/>
);
}
@@ -98,6 +98,12 @@ export type CheckoutWidgetProps = {
className?: string;
/**
* Whether to show thirdweb branding in the widget.
* @default true
*/
showThirdwebBranding?: boolean;
/**
* The chain the accepted token is on.
*/
@@ -353,6 +359,7 @@ export function CheckoutWidget(props: CheckoutWidgetProps) {
presetOptions={props.presetOptions}
purchaseData={props.purchaseData}
receiverAddress={props.seller}
showThirdwebBranding={props.showThirdwebBranding}
uiOptions={bridgeDataQuery.data.data}
/>
);
@@ -39,6 +39,12 @@ export interface DirectPaymentProps {
* Connect options for wallet connection
*/
connectOptions?: PayEmbedConnectOptions;
/**
* Whether to show thirdweb branding in the widget.
* @default true
*/
showThirdwebBranding?: boolean;
}
export function DirectPayment({
@@ -46,6 +52,7 @@ export function DirectPayment({
client,
onContinue,
connectOptions,
showThirdwebBranding = true,
}: DirectPaymentProps) {
const activeAccount = useActiveAccount();
const chain = defineChain(uiOptions.paymentInfo.token.chainId);
@@ -224,9 +231,12 @@ export function DirectPayment({
/>
)}
<Spacer y="md" />
<PoweredByThirdweb />
{showThirdwebBranding ? (
<div>
<Spacer y="md" />
<PoweredByThirdweb />
</div>
) : null}
<Spacer y="lg" />
</Container>
</WithHeader>
@@ -56,6 +56,12 @@ export interface FundWalletProps {
* Connect options for wallet connection
*/
connectOptions?: PayEmbedConnectOptions;
/**
* Whether to show thirdweb branding in the widget.
* @default true
*/
showThirdwebBranding?: boolean;
}
export function FundWallet({
@@ -65,6 +71,7 @@ export function FundWallet({
onContinue,
presetOptions = [5, 10, 20],
connectOptions,
showThirdwebBranding = true,
}: FundWalletProps) {
const [amount, setAmount] = useState(uiOptions.initialAmount ?? "");
const theme = useCustomTheme();
@@ -335,9 +342,12 @@ export function FundWallet({
/>
)}
<Spacer y="md" />
<PoweredByThirdweb />
{showThirdwebBranding ? (
<div>
<Spacer y="md" />
<PoweredByThirdweb />
</div>
) : null}
<Spacer y="lg" />
</WithHeader>
);
@@ -48,6 +48,12 @@ export interface TransactionPaymentProps {
* Connect options for wallet connection
*/
connectOptions?: PayEmbedConnectOptions;
/**
* Whether to show thirdweb branding in the widget.
* @default true
*/
showThirdwebBranding?: boolean;
}
export function TransactionPayment({
@@ -55,6 +61,7 @@ export function TransactionPayment({
client,
onContinue,
connectOptions,
showThirdwebBranding = true,
}: TransactionPaymentProps) {
const theme = useCustomTheme();
const activeAccount = useActiveAccount();
@@ -121,10 +128,13 @@ export function TransactionPayment({
}}
/>
<Spacer y="md" />
<PoweredByThirdweb />
<Spacer y="md" />
{showThirdwebBranding ? (
<div>
<Spacer y="md" />
<PoweredByThirdweb />
<Spacer y="md" />
</div>
) : null}
</WithHeader>
);
}
@@ -342,9 +352,12 @@ export function TransactionPayment({
/>
)}
<Spacer y="md" />
<PoweredByThirdweb />
{showThirdwebBranding ? (
<div>
<Spacer y="md" />
<PoweredByThirdweb />
</div>
) : null}
<Spacer y="lg" />
</WithHeader>
);
@@ -106,6 +106,12 @@ export type TransactionWidgetProps = {
className?: string;
/**
* Whether to show thirdweb branding in the widget.
* @default true
*/
showThirdwebBranding?: boolean;
/**
* The token address needed to complete this transaction. Leave undefined if no token is required.
*/
@@ -413,6 +419,7 @@ export function TransactionWidget(props: TransactionWidgetProps) {
purchaseData={props.purchaseData}
receiverAddress={undefined}
uiOptions={bridgeDataQuery.data.data}
showThirdwebBranding={props.showThirdwebBranding}
/>
);
}
@@ -77,6 +77,11 @@ export interface PaymentSelectionProps {
* @default ["crypto", "card"]
*/
paymentMethods?: ("crypto" | "card")[];
/**
* Fee payer
*/
feePayer?: "sender" | "receiver";
}
type Step =
@@ -97,6 +102,7 @@ export function PaymentSelection({
connectLocale,
includeDestinationToken,
paymentMethods = ["crypto", "card"],
feePayer,
}: PaymentSelectionProps) {
const connectedWallets = useConnectedWallets();
const activeWallet = useActiveWallet();
@@ -113,6 +119,7 @@ export function PaymentSelection({
toChainId: destinationToken.chainId,
toToken: destinationToken.address,
});
return true;
},
queryKey: ["payment_selection"],
});
@@ -267,6 +274,7 @@ export function PaymentSelection({
destinationToken.decimals,
)}
destinationToken={destinationToken}
feePayer={feePayer}
onBack={handleBackToWalletSelection}
onPaymentMethodSelected={handlePaymentMethodSelected}
paymentMethods={suitableTokenPaymentMethods}
@@ -21,6 +21,7 @@ interface TokenSelectionProps {
onBack: () => void;
destinationToken: Token;
destinationAmount: bigint;
feePayer?: "sender" | "receiver";
}
// Individual payment method token row component
@@ -30,6 +31,7 @@ interface PaymentMethodTokenRowProps {
destinationAmount: bigint;
client: ThirdwebClient;
onPaymentMethodSelected: (paymentMethod: PaymentMethod) => void;
feePayer?: "sender" | "receiver";
}
function PaymentMethodTokenRow({
@@ -38,6 +40,7 @@ function PaymentMethodTokenRow({
destinationAmount,
client,
onPaymentMethodSelected,
feePayer,
}: PaymentMethodTokenRowProps) {
const theme = useCustomTheme();
@@ -50,6 +53,7 @@ function PaymentMethodTokenRow({
client,
destinationAmount,
destinationToken,
feePayer,
originToken: paymentMethod.originToken,
});
@@ -158,6 +162,7 @@ export function TokenSelection({
onBack,
destinationToken,
destinationAmount,
feePayer,
}: TokenSelectionProps) {
const theme = useCustomTheme();
@@ -271,6 +276,7 @@ export function TokenSelection({
client={client}
destinationAmount={destinationAmount}
destinationToken={destinationToken}
feePayer={feePayer}
key={`${method.originToken.address}-${method.originToken.chainId}`}
onPaymentMethodSelected={onPaymentMethodSelected}
paymentMethod={method}
@@ -20,6 +20,7 @@ import {
} from "react";
import { trackPayEvent } from "../../../../analytics/track/pay.js";
import type { Chain } from "../../../../chains/types.js";
import { getCachedChain } from "../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../client/client.js";
import { getContract } from "../../../../contract/contract.js";
import type { SupportedFiatCurrency } from "../../../../pay/convert/type.js";
@@ -61,7 +62,10 @@ import type {
ConnectButtonProps,
PayUIOptions,
} from "../../../core/hooks/connection/ConnectButtonProps.js";
import { useChainFaucets } from "../../../core/hooks/others/useChainQuery.js";
import {
useChainFaucets,
useChainMetadata,
} from "../../../core/hooks/others/useChainQuery.js";
import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js";
import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js";
import { useActiveWalletChain } from "../../../core/hooks/wallets/useActiveWalletChain.js";
@@ -80,6 +84,7 @@ import type {
} from "../../../core/utils/defaultTokens.js";
import { useWalletInfo } from "../../../core/utils/wallet.js";
import { WalletUIStatesProvider } from "../../providers/wallet-ui-states-provider.js";
import { BuyWidget } from "../Bridge/BuyWidget.js";
import { Container, Line } from "../components/basic.js";
import { Button, IconButton } from "../components/buttons.js";
import { ChainActiveDot } from "../components/ChainActiveDot.js";
@@ -116,7 +121,6 @@ import {
NetworkSelectorContent,
type NetworkSelectorProps,
} from "./NetworkSelector.js";
import { LazyBuyScreen } from "./screens/Buy/LazyBuyScreen.js";
import { WalletManagerScreen } from "./screens/Details/WalletManagerScreen.js";
import { LinkedProfilesScreen } from "./screens/LinkedProfilesScreen.js";
import { LinkProfileScreen } from "./screens/LinkProfileScreen.js";
@@ -390,6 +394,7 @@ export function DetailsModal(props: {
const activeWallet = useActiveWallet();
const chainFaucetsQuery = useChainFaucets(walletChain);
const chainMetadataQuery = useChainMetadata(walletChain);
const disableSwitchChain = !activeWallet?.switchChain;
@@ -622,33 +627,35 @@ export function DetailsModal(props: {
</Button>
)}
{!hideBuyFunds && (
<Button
onClick={() => {
trackPayEvent({
client: client,
event: "details_modal_buy_click",
walletAddress: activeAccount?.address,
walletType: activeWallet?.id,
});
setScreen("buy");
}}
style={{
alignItems: "center",
display: "flex",
flex: 1,
fontSize: fontSize.sm,
gap: spacing.xs,
padding: spacing.sm,
}}
variant="outline"
>
<Container center="both" color="secondaryText" flex="row">
<PlusIcon height={iconSize.sm} width={iconSize.sm} />
</Container>
{locale.buy}
</Button>
)}
{!hideBuyFunds &&
chainMetadataQuery.data &&
!chainMetadataQuery.data.testnet && (
<Button
onClick={() => {
trackPayEvent({
client: client,
event: "details_modal_buy_click",
walletAddress: activeAccount?.address,
walletType: activeWallet?.id,
});
setScreen("buy");
}}
style={{
alignItems: "center",
display: "flex",
flex: 1,
fontSize: fontSize.sm,
gap: spacing.xs,
padding: spacing.sm,
}}
variant="outline"
>
<Container center="both" color="secondaryText" flex="row">
<PlusIcon height={iconSize.sm} width={iconSize.sm} />
</Container>
{locale.buy}
</Button>
)}
</Container>
</Container>
@@ -956,23 +963,26 @@ export function DetailsModal(props: {
// thirdweb pay
else if (screen === "buy") {
const requestedChainId =
props.detailsModal?.payOptions?.prefillBuy?.chain?.id ||
walletChain?.id ||
props.chains[0]?.id ||
1;
content = (
<LazyBuyScreen
<BuyWidget
amount={props.detailsModal?.payOptions?.prefillBuy?.amount || "0.01"}
chain={getCachedChain(requestedChainId)}
client={client}
connectLocale={locale}
connectOptions={undefined}
hiddenWallets={props.detailsModal?.hiddenWallets}
isEmbed={false}
onBack={() => setScreen("main")}
onDone={closeModal}
payOptions={
props.detailsModal?.payOptions || {
mode: "fund_wallet",
}
}
locale={locale.id}
onCancel={() => setScreen("main")}
onSuccess={() => setScreen("main")}
supportedTokens={props.supportedTokens}
theme={typeof props.theme === "string" ? props.theme : props.theme.type}
title={locale.buy}
tokenAddress={
props.displayBalanceToken?.[Number(requestedChainId)] as
| `0x${string}`
| undefined
}
/>
);
}
@@ -690,12 +690,10 @@ export const ChainButton = /* @__PURE__ */ memo(function ChainButton(props: {
)}
</Container>
{confirming || switchingFailed ? (
<div
style={{
display: "flex",
flexDirection: "column",
gap: spacing.xs,
}}
<Container
flex="column"
gap="3xs"
style={{ alignItems: "flex-start", width: "100%" }}
>
<ChainName
loadingComponent={<Skeleton height="20px" width="150px" />}
@@ -718,7 +716,7 @@ export const ChainButton = /* @__PURE__ */ memo(function ChainButton(props: {
</Container>
)}
</Container>
</div>
</Container>
) : (
<ChainName
className="tw-chain-icon-none-confirming"
@@ -102,6 +102,7 @@ export const WalletButtonEl = /* @__PURE__ */ StyledButton((_) => {
'&[data-active="true"]': {
backgroundColor: theme.colors.tertiaryBg,
},
all: "unset",
alignItems: "center",
borderRadius: radius.md,
boxSizing: "border-box",
File diff suppressed because it is too large Load Diff
@@ -1,286 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { trackPayEvent } from "../../../../../../analytics/track/pay.js";
import type { Chain } from "../../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js";
import { getContract } from "../../../../../../contract/contract.js";
import { decimals } from "../../../../../../extensions/erc20/read/decimals.js";
import { shortenAddress } from "../../../../../../utils/address.js";
import { formatNumber } from "../../../../../../utils/formatNumber.js";
import { toTokens } from "../../../../../../utils/units.js";
import type { Account } from "../../../../../../wallets/interfaces/wallet.js";
import { useCustomTheme } from "../../../../../core/design-system/CustomThemeProvider.js";
import { iconSize, spacing } from "../../../../../core/design-system/index.js";
import type { PayUIOptions } from "../../../../../core/hooks/connection/ConnectButtonProps.js";
import { useChainMetadata } from "../../../../../core/hooks/others/useChainQuery.js";
import { useActiveWallet } from "../../../../../core/hooks/wallets/useActiveWallet.js";
import type { TokenInfo } from "../../../../../core/utils/defaultTokens.js";
import { useEnsName } from "../../../../../core/utils/wallet.js";
import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js";
import { Container, Line, ModalHeader } from "../../../components/basic.js";
import { Button } from "../../../components/buttons.js";
import { ChainIcon } from "../../../components/ChainIcon.js";
import { Img } from "../../../components/Img.js";
import { Spacer } from "../../../components/Spacer.js";
import { TokenIcon } from "../../../components/TokenIcon.js";
import { Text } from "../../../components/text.js";
import { WalletImage } from "../../../components/WalletImage.js";
import type { PayEmbedConnectOptions } from "../../../PayEmbed.js";
import { ConnectButton } from "../../ConnectButton.js";
import { PoweredByThirdweb } from "../../PoweredByTW.js";
import { type ERC20OrNativeToken, isNativeToken } from "../nativeToken.js";
import type { SupportedChainAndTokens } from "./swap/useSwapSupportedChains.js";
export function DirectPaymentModeScreen(props: {
client: ThirdwebClient;
payUiOptions: Extract<PayUIOptions, { mode: "direct_payment" }>;
supportedDestinations: SupportedChainAndTokens;
payerAccount: Account | undefined;
connectOptions: PayEmbedConnectOptions | undefined;
onContinue: (
tokenAmount: string,
toChain: Chain,
toToken: ERC20OrNativeToken,
) => void;
}) {
const {
payUiOptions,
supportedDestinations,
client,
onContinue,
payerAccount,
} = props;
const theme = useCustomTheme();
const activeWallet = useActiveWallet();
const metadata = payUiOptions.metadata;
const paymentInfo = payUiOptions.paymentInfo;
const { data: chainData } = useChainMetadata(paymentInfo.chain);
const { data: sellerEns } = useEnsName({
address: paymentInfo.sellerAddress,
client,
});
const totalCostQuery = useQuery({
queryFn: async () => {
let tokenDecimals = 18;
if (paymentInfo.token && !isNativeToken(paymentInfo.token)) {
tokenDecimals = await decimals({
contract: getContract({
address: paymentInfo.token.address,
chain: paymentInfo.chain,
client,
}),
});
}
let cost: string;
if ("amountWei" in paymentInfo) {
cost = toTokens(paymentInfo.amountWei, tokenDecimals);
} else {
cost = paymentInfo.amount;
}
return cost;
},
queryKey: ["amount", paymentInfo],
});
const totalCost = totalCostQuery.data;
if (!chainData || totalCost === undefined) {
return <LoadingScreen />;
}
const token: TokenInfo = paymentInfo.token
? {
address: paymentInfo.token.address || NATIVE_TOKEN_ADDRESS,
icon:
paymentInfo.token?.icon ||
supportedDestinations
.find((c) => c.chain.id === paymentInfo.chain.id)
?.tokens.find(
(t) =>
t.address.toLowerCase() ===
paymentInfo.token?.address.toLowerCase(),
)?.icon,
name: paymentInfo.token.name || chainData.nativeCurrency.name,
symbol: paymentInfo.token.symbol || chainData.nativeCurrency.symbol,
}
: {
address: NATIVE_TOKEN_ADDRESS,
icon: chainData.icon?.url,
name: chainData.nativeCurrency.name,
symbol: chainData.nativeCurrency.symbol,
};
return (
<Container px="lg">
<Spacer y="lg" />
<ModalHeader title={metadata?.name || "Payment Details"} />
<Spacer y="lg" />
<Container>
{metadata?.image ? (
<Img
client={client}
src={metadata?.image}
style={{
backgroundColor: theme.colors.tertiaryBg,
borderRadius: spacing.md,
width: "100%",
}}
/>
) : activeWallet ? (
<Container
center="both"
flex="row"
style={{
backgroundColor: theme.colors.tertiaryBg,
borderRadius: spacing.md,
marginBottom: spacing.md,
padding: spacing.md,
}}
>
<WalletImage
client={client}
id={activeWallet.id}
size={iconSize.xl}
/>
<div
style={{
borderBottom: "6px dotted",
borderColor: theme.colors.secondaryIconColor,
flexGrow: 1,
marginLeft: spacing.md,
marginRight: spacing.md,
}}
/>
<ChainIcon
chainIconUrl={chainData.icon?.url}
client={client}
size={iconSize.xl}
/>
</Container>
) : null}
<Spacer y="md" />
<Container flex="row">
<Container expand flex="column">
<Text color="primaryText" size="md" weight={700}>
Price
</Text>
</Container>
<Container expand>
<Container
center="y"
flex="row"
gap="xs"
style={{ justifyContent: "right" }}
>
<TokenIcon
chain={paymentInfo.chain}
client={props.client}
size="sm"
token={token}
/>
<Text color="primaryText" size="md" weight={700}>
{String(formatNumber(Number(totalCost), 6))} {token.symbol}
</Text>
</Container>
</Container>
</Container>
<Spacer y="md" />
<Line />
<Spacer y="md" />
<Container flex="row">
<Container expand flex="column">
<Text color="secondaryText" size="xs">
Network
</Text>
</Container>
<Container expand>
<Container
center="y"
flex="row"
gap="xs"
style={{ justifyContent: "right" }}
>
<ChainIcon
chainIconUrl={chainData.icon?.url}
client={props.client}
size="xs"
/>
<Text
color="secondaryText"
size="xs"
style={{ textAlign: "right" }}
>
{chainData.name}
</Text>
</Container>
</Container>
</Container>
<Spacer y="sm" />
<Container flex="row">
<Container expand flex="column">
<Text color="secondaryText" size="xs">
Seller
</Text>
</Container>
<Container expand>
<Container
center="y"
flex="row"
gap="xs"
style={{ justifyContent: "right" }}
>
<Text
color="secondaryText"
size="xs"
style={{ textAlign: "right" }}
>
{sellerEns || shortenAddress(paymentInfo.sellerAddress)}
</Text>
</Container>
</Container>
</Container>
</Container>
<Spacer y="xl" />
{payerAccount ? (
<Button
fullWidth
onClick={() => {
trackPayEvent({
client,
event: "choose_payment_method_direct_payment_mode",
toChainId: paymentInfo.chain.id,
toToken: paymentInfo.token?.address,
walletAddress: payerAccount.address,
walletType: activeWallet?.id,
});
onContinue(totalCost, paymentInfo.chain, token);
}}
variant="accent"
>
Choose Payment Method
</Button>
) : (
<div>
<ConnectButton
{...props.connectOptions}
client={client}
connectButton={{
style: {
width: "100%",
},
}}
theme={theme}
/>
</div>
)}
<Spacer y="lg" />
{payUiOptions.showThirdwebBranding !== false && (
<>
<PoweredByThirdweb link="https://playground.thirdweb.com/connect/pay?utm_source=ub_text" />
<Spacer y="sm" />
</>
)}
</Container>
);
}
@@ -1,85 +0,0 @@
import { ClockIcon } from "@radix-ui/react-icons";
import {
fontSize,
iconSize,
radius,
} from "../../../../../core/design-system/index.js";
import { Container } from "../../../components/basic.js";
import { Button } from "../../../components/buttons.js";
import { Skeleton } from "../../../components/Skeleton.js";
import { Text } from "../../../components/text.js";
import type { IconFC } from "../../icons/types.js";
import { formatSeconds } from "./swap/formatSeconds.js";
export function EstimatedTimeAndFees(props: {
estimatedSeconds?: number | undefined;
quoteIsLoading: boolean;
onViewFees: () => void;
}) {
const { estimatedSeconds, quoteIsLoading } = props;
return (
<Container
bg="tertiaryBg"
borderColor="borderColor"
flex="row"
style={{
alignItems: "center",
borderRadius: radius.md,
borderStyle: "solid",
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderWidth: "1px",
justifyContent: "space-between",
}}
>
<Container center="y" color="accentText" flex="row" gap="xxs" p="sm">
<ClockIcon height={iconSize.sm} width={iconSize.sm} />
{quoteIsLoading ? (
<Skeleton color="borderColor" height={fontSize.xs} width="50px" />
) : (
<Text color="secondaryText" size="xs">
{estimatedSeconds !== undefined
? `~${formatSeconds(estimatedSeconds)}`
: "--"}
</Text>
)}
</Container>
<Button gap="xs" onClick={props.onViewFees} variant="ghost">
<Container center="both" color="accentText" flex="row">
<ViewFeeIcon size={iconSize.sm} />
</Container>
<Text color="secondaryText" size="xs">
View Fees
</Text>
</Button>
</Container>
);
}
const ViewFeeIcon: IconFC = (props) => {
return (
<svg
aria-hidden="true"
fill="none"
height={props.size}
viewBox="0 0 12 12"
width={props.size}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.5 1.5H2.5C1.94772 1.5 1.5 1.94772 1.5 2.5V9.5C1.5 10.0523 1.94772 10.5 2.5 10.5H9.5C10.0523 10.5 10.5 10.0523 10.5 9.5V2.5C10.5 1.94772 10.0523 1.5 9.5 1.5Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.5 7.5L7.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
@@ -1,13 +0,0 @@
import { lazy, Suspense } from "react";
import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js";
import type { BuyScreenProps } from "./BuyScreen.js";
const BuyScreen = lazy(() => import("./BuyScreen.js"));
export function LazyBuyScreen(props: BuyScreenProps) {
return (
<Suspense fallback={<LoadingScreen />}>
<BuyScreen {...props} />
</Suspense>
);
}
@@ -1,68 +0,0 @@
import { ChevronDownIcon } from "@radix-ui/react-icons";
import type { FiatProvider } from "../../../../../../pay/utils/commonTypes.js";
import { iconSize, spacing } from "../../../../../core/design-system/index.js";
import { Container } from "../../../components/basic.js";
import { Button } from "../../../components/buttons.js";
import { Text } from "../../../components/text.js";
import { getProviderLabel } from "./utils.js";
/**
* Shows the selected payment provider based on the preferred provider OR the quoted provider.
* @internal
*/
export const PayProviderSelection = (props: {
onShowProviders: () => void;
quotedProvider?: FiatProvider;
preferredProvider?: FiatProvider;
supportedProviders: FiatProvider[];
}) => {
const ProviderItem = (
<Container
center="y"
color="secondaryText"
flex="row"
gap="xxs"
style={{ padding: spacing.md }}
>
<Text size="xs">
{getProviderLabel(
props.preferredProvider ?? props.quotedProvider ?? "",
)}
</Text>
{props.supportedProviders.length > 1 && (
<ChevronDownIcon height={iconSize.sm} width={iconSize.sm} />
)}
</Container>
);
return (
<Container
bg="tertiaryBg"
borderColor="borderColor"
flex="row"
style={{
alignItems: "center",
borderBottom: "none",
borderStyle: "solid",
borderWidth: "1px",
justifyContent: "space-between",
paddingLeft: spacing.md,
}}
>
<Text color="secondaryText" size="xs">
Provider
</Text>
{props.supportedProviders.length > 1 ? (
<Button
onClick={props.onShowProviders}
style={{ padding: 0 }}
variant="ghost" // Padding is managed within children as the button is conditional
>
{ProviderItem}
</Button>
) : (
ProviderItem
)}
</Container>
);
};
@@ -1,48 +0,0 @@
import type { Chain } from "../../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../../client/client.js";
import { getAddress } from "../../../../../../utils/address.js";
import type { iconSize } from "../../../../../core/design-system/index.js";
import { TokenIcon } from "../../../components/TokenIcon.js";
import { isNativeToken, type NativeToken } from "../nativeToken.js";
import { useBuySupportedDestinations } from "./swap/useSwapSupportedChains.js";
// This is a temporary solution to get the token icon working for ERC20 tokens
// TODO: The proper solutioon is to include the token icon in the quotes / status response objects, currently it is missing.
export function PayTokenIcon(props: {
token:
| {
address: string;
icon?: string;
}
| NativeToken;
chain: Chain;
client: ThirdwebClient;
size: keyof typeof iconSize;
}) {
const supportedDestinationsQuery = useBuySupportedDestinations(props.client);
const token = props.token;
const tokenIcon = !isNativeToken(token)
? supportedDestinationsQuery.data
?.find((c) => c.chain.id === props.chain.id)
?.tokens.find(
(t) => getAddress(t.address) === getAddress(token.address),
)?.icon
: undefined;
return (
<TokenIcon
chain={props.chain}
client={props.client}
size={props.size}
token={
isNativeToken(token)
? { nativeToken: true }
: {
address: token.address,
icon: token.icon || tokenIcon,
}
}
/>
);
}
@@ -1,104 +0,0 @@
import styled from "@emotion/styled";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import type { ThirdwebClient } from "../../../../../../client/client.js";
import { formatNumber } from "../../../../../../utils/formatNumber.js";
import {
fontSize,
iconSize,
radius,
spacing,
} from "../../../../../core/design-system/index.js";
import { Container } from "../../../components/basic.js";
import { Button } from "../../../components/buttons.js";
import { Skeleton } from "../../../components/Skeleton.js";
import { Text } from "../../../components/text.js";
import { type CurrencyMeta, getFiatIcon } from "./fiat/currencies.js";
/**
* Shows an amount "value" and renders the selected token and chain
* It also renders the buttons to select the token and chain
* It also renders the balance of active wallet for the selected token in selected chain
* @internal
*/
export function PayWithCreditCard(props: {
value?: string;
isLoading: boolean;
client: ThirdwebClient;
currency: CurrencyMeta;
onSelectCurrency: () => void;
}) {
return (
<Container
bg="tertiaryBg"
borderColor="borderColor"
flex="row"
style={{
alignItems: "center",
borderBottom: "none",
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
borderRadius: radius.md,
borderStyle: "solid",
borderWidth: "1px",
flexWrap: "nowrap",
justifyContent: "space-between",
}}
>
{/* Left */}
<CurrencyButton
gap="sm"
onClick={props.onSelectCurrency}
style={{
justifyContent: "flex-start",
minHeight: "64px",
minWidth: "50%",
}}
variant="ghost"
>
{getFiatIcon(props.currency, "md")}
<Container center="y" color="secondaryText" flex="row" gap="xxs">
<Text color="primaryText">{props.currency.shorthand}</Text>
<ChevronDownIcon height={iconSize.sm} width={iconSize.sm} />
</Container>
</CurrencyButton>
{/* Right */}
<div
style={{
alignItems: "flex-end",
display: "flex",
flexDirection: "column",
flexGrow: 1,
flexShrink: 1,
gap: spacing.xxs,
justifyContent: "center",
overflow: "hidden",
paddingRight: spacing.sm,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{props.isLoading ? (
<Skeleton height={fontSize.lg} width="100px" />
) : (
<Text color={props.value ? "primaryText" : "secondaryText"} size="lg">
{props.value
? `${props.currency.symbol}${formatNumber(
Number(props.value),
2,
)}`
: "--"}
</Text>
)}
</div>
</Container>
);
}
const CurrencyButton = /* @__PURE__ */ styled(Button)(() => {
return {
"&[disabled]:hover": {
borderColor: "transparent",
},
};
});
@@ -1,91 +0,0 @@
import { keyframes } from "@emotion/react";
import { CheckIcon } from "@radix-ui/react-icons";
import { fontSize, iconSize } from "../../../../../core/design-system/index.js";
import { Container } from "../../../components/basic.js";
import { StyledDiv } from "../../../design-system/elements.js";
export function StepIcon(props: { isDone: boolean; isActive: boolean }) {
return (
<Container
center="both"
color={
props.isDone
? "success"
: props.isActive
? "accentText"
: "secondaryText"
}
flex="row"
>
<Circle>
{props.isDone ? (
<CheckIcon height={iconSize.sm} width={iconSize.sm} />
) : (
<PulsingDot data-active={props.isActive} />
)}
</Circle>
</Container>
);
}
export function Step(props: {
isDone: boolean;
label: string;
isActive: boolean;
}) {
return (
<Container
center="y"
color={
props.isDone
? "success"
: props.isActive
? "accentText"
: "secondaryText"
}
flex="row"
gap="xs"
style={{
fontSize: fontSize.sm,
}}
>
<StepIcon isActive={props.isActive} isDone={props.isDone} />
{props.label}
</Container>
);
}
const pulseAnimation = keyframes`
0% {
opacity: 1;
transform: scale(0.5);
}
100% {
opacity: 0;
transform: scale(1.5);
}
`;
const PulsingDot = /* @__PURE__ */ StyledDiv(() => {
return {
'&[data-active="true"]': {
animation: `${pulseAnimation} 1s infinite`,
},
background: "currentColor",
borderRadius: "50%",
height: "10px",
width: "10px",
};
});
const Circle = /* @__PURE__ */ StyledDiv(() => {
return {
alignItems: "center",
border: "1px solid currentColor",
borderRadius: "50%",
display: "flex",
height: "20px",
justifyContent: "center",
width: "20px",
};
});
@@ -1,395 +0,0 @@
import { trackPayEvent } from "../../../../../../analytics/track/pay.js";
import type { Chain } from "../../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js";
import { formatNumber } from "../../../../../../utils/formatNumber.js";
import { toTokens } from "../../../../../../utils/units.js";
import type { Account } from "../../../../../../wallets/interfaces/wallet.js";
import { hasSponsoredTransactionsEnabled } from "../../../../../../wallets/smart/is-smart-wallet.js";
import { useCustomTheme } from "../../../../../core/design-system/CustomThemeProvider.js";
import {
fontSize,
iconSize,
spacing,
} from "../../../../../core/design-system/index.js";
import type { PayUIOptions } from "../../../../../core/hooks/connection/ConnectButtonProps.js";
import { useChainMetadata } from "../../../../../core/hooks/others/useChainQuery.js";
import { useWalletBalance } from "../../../../../core/hooks/others/useWalletBalance.js";
import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js";
import { useActiveWallet } from "../../../../../core/hooks/wallets/useActiveWallet.js";
import { ErrorState } from "../../../../wallets/shared/ErrorState.js";
import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js";
import { Container, Line, ModalHeader } from "../../../components/basic.js";
import { Button } from "../../../components/buttons.js";
import { ChainIcon } from "../../../components/ChainIcon.js";
import { Img } from "../../../components/Img.js";
import { Skeleton } from "../../../components/Skeleton.js";
import { Spacer } from "../../../components/Spacer.js";
import { TokenIcon } from "../../../components/TokenIcon.js";
import { Text } from "../../../components/text.js";
import { TokenSymbol } from "../../../components/token/TokenSymbol.js";
import type { PayEmbedConnectOptions } from "../../../PayEmbed.js";
import { ConnectButton } from "../../ConnectButton.js";
import { OutlineWalletIcon } from "../../icons/OutlineWalletIcon.js";
import { PoweredByThirdweb } from "../../PoweredByTW.js";
import { formatTokenBalance } from "../formatTokenBalance.js";
import {
type ERC20OrNativeToken,
isNativeToken,
NATIVE_TOKEN,
} from "../nativeToken.js";
import { useTransactionCostAndData } from "./main/useBuyTxStates.js";
import type { SupportedChainAndTokens } from "./swap/useSwapSupportedChains.js";
import { WalletRow } from "./swap/WalletRow.js";
export function TransactionModeScreen(props: {
client: ThirdwebClient;
payUiOptions: Extract<PayUIOptions, { mode: "transaction" }>;
supportedDestinations: SupportedChainAndTokens;
payerAccount: Account | undefined;
connectOptions: PayEmbedConnectOptions | undefined;
onContinue: (
tokenAmount: string,
toChain: Chain,
toToken: ERC20OrNativeToken,
) => void;
}) {
const {
payUiOptions,
client,
payerAccount,
supportedDestinations,
onContinue,
} = props;
const {
data: chainData,
error: chainDataError,
isLoading: chainDataLoading,
refetch: chainDataRefetch,
} = useChainMetadata(payUiOptions.transaction.chain);
const metadata = payUiOptions.metadata;
const {
data: transactionCostAndData,
error: transactionCostAndDataError,
isLoading: transactionCostAndDataLoading,
refetch: transactionCostAndDataRefetch,
} = useTransactionCostAndData({
account: payerAccount,
supportedDestinations,
transaction: payUiOptions.transaction,
});
const theme = useCustomTheme();
const activeWallet = useActiveWallet();
const activeAccount = useActiveAccount();
const sponsoredTransactionsEnabled =
hasSponsoredTransactionsEnabled(activeWallet);
const balanceQuery = useWalletBalance(
{
address: activeAccount?.address,
chain: payUiOptions.transaction.chain,
client: props.client,
tokenAddress: isNativeToken(transactionCostAndData?.token || NATIVE_TOKEN)
? undefined
: transactionCostAndData?.token.address,
},
{
enabled: !!transactionCostAndData,
},
);
if (transactionCostAndDataLoading || chainDataLoading) {
return <LoadingScreen />;
}
if (!activeAccount) {
return (
<Container
center="both"
flex="row"
fullHeight
style={{
minHeight: "350px",
}}
>
<Container animate="fadein">
<Spacer y="xxl" />
<Container center="x" flex="row">
<OutlineWalletIcon size={iconSize["3xl"]} />
</Container>
<Spacer y="lg" />
<Text center color="primaryText" size="md">
Please connect a wallet to continue
</Text>
<Spacer y="xl" />
<Container center="x" flex="row" style={{ width: "100%" }}>
<ConnectButton
client={client}
theme={theme}
{...props.connectOptions}
/>
</Container>
</Container>
</Container>
);
}
if (transactionCostAndDataError || chainDataError) {
return (
<Container
center="both"
flex="row"
fullHeight
style={{
minHeight: "350px",
}}
>
<ErrorState
onTryAgain={
transactionCostAndDataError
? transactionCostAndDataRefetch
: chainDataRefetch
}
title={
transactionCostAndDataError?.message ||
chainDataError?.message ||
"Something went wrong"
}
/>
</Container>
);
}
if (!transactionCostAndData || !chainData) {
return <LoadingScreen />;
}
const insufficientFunds =
balanceQuery.data &&
balanceQuery.data.value < transactionCostAndData.transactionValueWei;
return (
<Container px="lg">
<Spacer y="lg" />
<ModalHeader title={metadata?.name || "Transaction"} />
<Spacer y="lg" />
<Container>
{metadata?.image ? (
<Img
client={client}
src={metadata?.image}
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: spacing.md,
width: "100%",
}}
/>
) : activeAccount ? (
<Container flex="column" gap="sm">
{insufficientFunds && (
<div>
<Text center color="danger" multiline size="xs">
Insufficient Funds
</Text>
<Text center multiline size="xs">
Select another token or pay with card.
</Text>
</div>
)}
<Container
flex="row"
style={{
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: spacing.md,
justifyContent: "space-between",
marginBottom: spacing.sm,
padding: spacing.sm,
}}
>
<WalletRow
address={activeAccount?.address}
client={client}
iconSize="md"
/>
{balanceQuery.data ? (
<Container center="y" flex="row" gap="3xs">
<Text color="secondaryText" size="xs" weight={500}>
{formatTokenBalance(balanceQuery.data, false)}
</Text>
<TokenSymbol
chain={payUiOptions.transaction.chain}
color="secondaryText"
size="xs"
token={transactionCostAndData.token}
/>
</Container>
) : (
<Skeleton height={fontSize.xs} width="70px" />
)}
</Container>
</Container>
) : null}
<Spacer y="md" />
<Container flex="row">
<Container expand flex="column">
<Text color="primaryText" size="md" weight={700}>
Price
</Text>
</Container>
<Container expand>
<Container
center="y"
flex="row"
gap="xs"
style={{ justifyContent: "right" }}
>
<TokenIcon
chain={payUiOptions.transaction.chain}
client={props.client}
size="sm"
token={transactionCostAndData.token}
/>
<Text color="primaryText" size="md" weight={700}>
{String(
formatNumber(
Number(
toTokens(
transactionCostAndData.transactionValueWei,
transactionCostAndData.decimals,
),
),
6,
),
)}{" "}
{transactionCostAndData.token.symbol}
</Text>
</Container>
</Container>
</Container>
<Spacer y="md" />
<Line />
<Spacer y="md" />
<Container flex="row">
<Container expand flex="column">
<Text color="secondaryText" size="xs">
Gas Fees
</Text>
</Container>
<Container expand>
<Container
center="y"
flex="row"
gap="xs"
style={{ justifyContent: "right" }}
>
<Text
color={sponsoredTransactionsEnabled ? "success" : "primaryText"}
size="xs"
>
{sponsoredTransactionsEnabled
? "Sponsored"
: `${String(
formatNumber(
Number(
toTokens(
transactionCostAndData.gasCostWei,
chainData.nativeCurrency.decimals,
),
),
6,
),
)} ${chainData.nativeCurrency.symbol}`}
</Text>
</Container>
</Container>
</Container>
<Spacer y="sm" />
<Container flex="row">
<Container expand flex="column">
<Text color="secondaryText" size="xs">
Network
</Text>
</Container>
<Container expand>
<Container
center="y"
flex="row"
gap="xs"
style={{ justifyContent: "right" }}
>
<ChainIcon
chainIconUrl={chainData.icon?.url}
client={props.client}
size="xs"
/>
<Text
color="secondaryText"
size="xs"
style={{ textAlign: "right" }}
>
{chainData.name}
</Text>
</Container>
</Container>
</Container>
</Container>
<Spacer y="xl" />
{payerAccount ? (
<Button
fullWidth
onClick={() => {
let totalCostWei = insufficientFunds
? transactionCostAndData.transactionValueWei -
(balanceQuery.data?.value || 0n)
: transactionCostAndData.transactionValueWei;
if (
transactionCostAndData.token.address === NATIVE_TOKEN_ADDRESS &&
!sponsoredTransactionsEnabled
) {
totalCostWei += transactionCostAndData.gasCostWei;
}
trackPayEvent({
amountWei: totalCostWei.toString(),
client,
event: "choose_payment_method_transaction_mode",
toChainId: payUiOptions.transaction.chain.id,
toToken: transactionCostAndData.token.address,
walletAddress: payerAccount.address,
walletType: activeWallet?.id,
});
onContinue(
toTokens(totalCostWei, transactionCostAndData.decimals),
payUiOptions.transaction.chain,
transactionCostAndData.token,
);
}}
variant="accent"
>
Choose Payment Method
</Button>
) : (
<div>
<ConnectButton
{...props.connectOptions}
client={client}
connectButton={{
style: {
width: "100%",
},
}}
theme={theme}
/>
</div>
)}
<Spacer y="lg" />
{payUiOptions.showThirdwebBranding !== false && (
<>
<PoweredByThirdweb link="https://playground.thirdweb.com/connect/pay?utm_source=ub_text" />
<Spacer y="sm" />
</>
)}
</Container>
);
}
@@ -1,61 +0,0 @@
import styled from "@emotion/styled";
import { useCustomTheme } from "../../../../../../core/design-system/CustomThemeProvider.js";
import { spacing } from "../../../../../../core/design-system/index.js";
import { Container, Line, ModalHeader } from "../../../../components/basic.js";
import { Button } from "../../../../components/buttons.js";
import { Spacer } from "../../../../components/Spacer.js";
import { Text } from "../../../../components/text.js";
import { type CurrencyMeta, currencies, getFiatIcon } from "./currencies.js";
export function CurrencySelection(props: {
onSelect: (currency: CurrencyMeta) => void;
onBack: () => void;
}) {
return (
<Container>
<Container p="lg">
<ModalHeader onBack={props.onBack} title="Pay with" />
</Container>
<Line />
<Spacer y="lg" />
<Container flex="column" gap="xs" px="lg">
{currencies.map((c) => {
return (
<SelectCurrencyButton
fullWidth
gap="sm"
key={c.shorthand}
onClick={() => props.onSelect(c)}
variant="secondary"
>
{getFiatIcon(c, "lg")}
<Container flex="column" gap="xxs">
<Text color="primaryText">{c.shorthand}</Text>
<Text size="sm">{c.name}</Text>
</Container>
</SelectCurrencyButton>
);
})}
</Container>
<Spacer y="lg" />
</Container>
);
}
const SelectCurrencyButton = /* @__PURE__ */ styled(Button)(() => {
const theme = useCustomTheme();
return {
"&:hover": {
background: theme.colors.secondaryButtonBg,
transform: "scale(1.01)",
},
background: theme.colors.tertiaryBg,
gap: spacing.sm,
justifyContent: "flex-start",
padding: spacing.sm,
transition: "background 200ms ease, transform 150ms ease",
};
});
@@ -1,263 +0,0 @@
import { useState } from "react";
import { trackPayEvent } from "../../../../../../../analytics/track/pay.js";
import type { Chain } from "../../../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
import {
type FiatProvider,
FiatProviders,
} from "../../../../../../../pay/utils/commonTypes.js";
import type { Theme } from "../../../../../../core/design-system/index.js";
import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
import { useBuyWithFiatQuote } from "../../../../../../core/hooks/pay/useBuyWithFiatQuote.js";
import { PREFERRED_FIAT_PROVIDER_STORAGE_KEY } from "../../../../../../core/utils/storage.js";
import { getErrorMessage } from "../../../../../utils/errors.js";
import { Container } from "../../../../components/basic.js";
import { Button } from "../../../../components/buttons.js";
import {
Drawer,
DrawerOverlay,
useDrawer,
} from "../../../../components/Drawer.js";
import { Spacer } from "../../../../components/Spacer.js";
import { Spinner } from "../../../../components/Spinner.js";
import { Text } from "../../../../components/text.js";
import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js";
import { EstimatedTimeAndFees } from "../EstimatedTimeAndFees.js";
import type { SelectedScreen } from "../main/types.js";
import { PayProviderSelection } from "../PayProviderSelection.js";
import { PayWithCreditCard } from "../PayWIthCreditCard.js";
import { FiatFees } from "../swap/Fees.js";
import type { PayerInfo } from "../types.js";
import type { CurrencyMeta } from "./currencies.js";
import { Providers } from "./Providers.js";
export function FiatScreenContent(props: {
setScreen: (screen: SelectedScreen) => void;
tokenAmount: string;
toToken: ERC20OrNativeToken;
toChain: Chain;
selectedCurrency: CurrencyMeta;
showCurrencySelector: () => void;
payOptions: PayUIOptions;
theme: "light" | "dark" | Theme;
client: ThirdwebClient;
onDone: () => void;
isEmbed: boolean;
payer: PayerInfo;
setTokenAmount: (amount: string) => void;
setHasEditedAmount: (hasEdited: boolean) => void;
paymentLinkId: undefined | string;
}) {
const {
toToken,
tokenAmount,
payer,
client,
setScreen,
toChain,
showCurrencySelector,
selectedCurrency,
paymentLinkId,
} = props;
const defaultRecipientAddress = (
props.payOptions as Extract<PayUIOptions, { mode: "direct_payment" }>
)?.paymentInfo?.sellerAddress;
const receiverAddress =
defaultRecipientAddress || props.payer.account.address;
const { drawerRef, drawerOverlayRef, isOpen, setIsOpen } = useDrawer();
const [drawerScreen, setDrawerScreen] = useState<"fees" | "providers">(
"fees",
);
const buyWithFiatOptions = props.payOptions.buyWithFiat;
const [preferredProvider, setPreferredProvider] = useState<
FiatProvider | undefined
>(
buyWithFiatOptions !== false
? buyWithFiatOptions?.preferredProvider ||
((localStorage.getItem(
PREFERRED_FIAT_PROVIDER_STORAGE_KEY,
) as FiatProvider | null) ??
undefined)
: undefined,
);
const supportedProviders = (() => {
if (!buyWithFiatOptions) return [...FiatProviders];
const options = buyWithFiatOptions?.supportedProviders ?? [];
const optionsWithPreferred =
options.length > 0
? new Set([
...(preferredProvider ? [preferredProvider] : []),
...options,
])
: FiatProviders;
return Array.from(optionsWithPreferred);
})();
const fiatQuoteQuery = useBuyWithFiatQuote(
buyWithFiatOptions !== false && tokenAmount
? {
client,
fromAddress: payer.account.address,
fromCurrencySymbol: selectedCurrency.shorthand,
isTestMode: buyWithFiatOptions?.testMode,
onrampChainId: buyWithFiatOptions?.onrampChainId,
onrampTokenAddress: buyWithFiatOptions?.onrampTokenAddress,
paymentLinkId: paymentLinkId,
preferredProvider: preferredProvider ?? supportedProviders[0],
purchaseData: props.payOptions.purchaseData,
toAddress: receiverAddress,
toAmount: tokenAmount,
toChainId: toChain.id,
toTokenAddress: isNativeToken(toToken)
? NATIVE_TOKEN_ADDRESS
: toToken.address,
}
: undefined,
);
function handleSubmit() {
if (!fiatQuoteQuery.data) {
return;
}
setScreen({
id: "fiat-flow",
quote: fiatQuoteQuery.data,
});
}
function showFees() {
if (!fiatQuoteQuery.data) {
return;
}
setDrawerScreen("fees");
setIsOpen(true);
}
function showProviders() {
setDrawerScreen("providers");
setIsOpen(true);
}
const disableSubmit = !fiatQuoteQuery.data;
const errorMsg =
!fiatQuoteQuery.isLoading && fiatQuoteQuery.error
? getErrorMessage(fiatQuoteQuery.error)
: undefined;
return (
<Container animate="fadein" flex="column" gap="lg">
{isOpen && (
<>
<DrawerOverlay ref={drawerOverlayRef} />
<Drawer close={() => setIsOpen(false)} ref={drawerRef}>
{drawerScreen === "fees" && fiatQuoteQuery.data && (
<div>
<Text color="primaryText" size="lg">
Fees
</Text>
<Spacer y="lg" />
<FiatFees quote={fiatQuoteQuery.data} />
</div>
)}
{drawerScreen === "providers" && (
<div>
<Text color="primaryText" size="lg">
Providers
</Text>
<Spacer y="lg" />
<Providers
onSelect={(provider) => {
setPreferredProvider(provider);
// save the pref in local storage
localStorage.setItem(
PREFERRED_FIAT_PROVIDER_STORAGE_KEY,
provider,
);
setIsOpen(false);
}}
preferredProvider={
preferredProvider || fiatQuoteQuery.data?.provider
}
supportedProviders={supportedProviders}
/>
</div>
)}
</Drawer>
</>
)}
<Container flex="column" gap="sm">
<Text size="sm">Pay with card</Text>
<div>
<PayWithCreditCard
client={client}
currency={selectedCurrency}
isLoading={fiatQuoteQuery.isLoading}
onSelectCurrency={showCurrencySelector}
value={fiatQuoteQuery.data?.fromCurrencyWithFees.amount}
/>
{/** Shows preferred or quoted provider and conditional selection */}
<PayProviderSelection
onShowProviders={showProviders}
preferredProvider={preferredProvider}
quotedProvider={fiatQuoteQuery.data?.provider}
supportedProviders={supportedProviders}
/>
{/* Estimated time + View fees button */}
<EstimatedTimeAndFees
estimatedSeconds={fiatQuoteQuery.data?.estimatedDurationSeconds}
onViewFees={showFees}
quoteIsLoading={fiatQuoteQuery.isLoading}
/>
</div>
{/* Error message */}
{errorMsg && (
<div>
<Text center color="danger" multiline size="xs">
{errorMsg.title}
</Text>
<Text center multiline size="xs">
{errorMsg.message}
</Text>
</div>
)}
</Container>
<Button
data-disabled={disableSubmit}
disabled={disableSubmit}
fullWidth
gap="xs"
onClick={() => {
trackPayEvent({
client: client,
event: "confirm_onramp_quote",
toChainId: toChain.id,
toToken: isNativeToken(toToken) ? undefined : toToken.address,
walletAddress: payer.account.address,
walletType: payer.wallet.id,
});
handleSubmit();
}}
variant={disableSubmit ? "outline" : "accent"}
>
{fiatQuoteQuery.isLoading ? (
<>
Getting price quote
<Spinner size="sm" />
</>
) : (
"Continue"
)}
</Button>
</Container>
);
}
@@ -1,85 +0,0 @@
import { Cross1Icon } from "@radix-ui/react-icons";
import {
iconSize,
radius,
spacing,
type Theme,
} from "../../../../../../core/design-system/index.js";
import { Container } from "../../../../components/basic.js";
import { Spinner } from "../../../../components/Spinner.js";
import { Text } from "../../../../components/text.js";
import type { FiatStatusMeta } from "../pay-transactions/statusMeta.js";
import { StepIcon } from "../Stepper.js";
export function StepContainer(props: {
state?: FiatStatusMeta["progressStatus"];
children: React.ReactNode;
style?: React.CSSProperties;
index?: number;
}) {
let color: keyof Theme["colors"] = "borderColor";
let text: string | undefined;
if (props.state === "pending") {
text = "Pending";
color = "accentText";
} else if (props.state === "actionRequired") {
color = "accentText";
} else if (props.state === "completed") {
text = "Completed";
color = "success";
} else if (props.state === "failed") {
color = "danger";
text = "Failed";
}
return (
<Container
bg="tertiaryBg"
borderColor={color === "success" ? "borderColor" : color}
px="md"
py="sm"
style={{
alignItems: "flex-start",
borderRadius: radius.lg,
borderStyle: "solid",
borderWidth: "1px",
position: "relative",
...props.style,
}}
>
{props.children}
<div
style={{
alignItems: "center",
display: "flex",
gap: spacing.xs,
position: "absolute",
right: spacing.xs,
top: spacing.xs,
}}
>
{props.state && text && (
<Text color={color} size="xs">
{text}
</Text>
)}
{(props.state === "actionRequired" || props.state === "completed") && (
<StepIcon
isActive={props.state === "actionRequired"}
isDone={props.state === "completed"}
/>
)}
{props.state === "pending" && <Spinner color="accentText" size="sm" />}
{props.state === "failed" && (
<Container center="both" color="danger" flex="row">
<Cross1Icon height={iconSize.sm} width={iconSize.sm} />
</Container>
)}
</div>
</Container>
);
}
@@ -1,775 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import { trackPayEvent } from "../../../../../../../analytics/track/pay.js";
import { getCachedChain } from "../../../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import { getContract } from "../../../../../../../contract/contract.js";
import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js";
import { approve } from "../../../../../../../extensions/erc20/write/approve.js";
import { getBuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.js";
import {
type BuyWithFiatStatus,
getBuyWithFiatStatus,
} from "../../../../../../../pay/buyWithFiat/getStatus.js";
import {
getOnRampSteps,
type OnRampStep,
} from "../../../../../../../pay/buyWithFiat/isSwapRequiredPostOnramp.js";
import type { PayTokenInfo } from "../../../../../../../pay/utils/commonTypes.js";
import { sendBatchTransaction } from "../../../../../../../transaction/actions/send-batch-transaction.js";
import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js";
import type { WaitForReceiptOptions } from "../../../../../../../transaction/actions/wait-for-tx-receipt.js";
import { waitForReceipt } from "../../../../../../../transaction/actions/wait-for-tx-receipt.js";
import { formatNumber } from "../../../../../../../utils/formatNumber.js";
import { isInAppSigner } from "../../../../../../../wallets/in-app/core/wallet/is-in-app-signer.js";
import { spacing } from "../../../../../../core/design-system/index.js";
import { useChainName } from "../../../../../../core/hooks/others/useChainQuery.js";
import { useBuyWithCryptoStatus } from "../../../../../../core/hooks/pay/useBuyWithCryptoStatus.js";
import { useBuyWithFiatStatus } from "../../../../../../core/hooks/pay/useBuyWithFiatStatus.js";
import { useConnectedWallets } from "../../../../../../core/hooks/wallets/useConnectedWallets.js";
import { invalidateWalletBalance } from "../../../../../../core/providers/invalidateWalletBalance.js";
import { Container, ModalHeader } from "../../../../components/basic.js";
import { Button } from "../../../../components/buttons.js";
import { Spacer } from "../../../../components/Spacer.js";
import { Spinner } from "../../../../components/Spinner.js";
import { SwitchNetworkButton } from "../../../../components/SwitchNetwork.js";
import { Text } from "../../../../components/text.js";
import { TokenSymbol } from "../../../../components/token/TokenSymbol.js";
import { openOnrampPopup } from "../openOnRamppopup.js";
import { PayTokenIcon } from "../PayTokenIcon.js";
import type { FiatStatusMeta } from "../pay-transactions/statusMeta.js";
import { addPendingTx } from "../swap/pendingSwapTx.js";
import { StepConnectorArrow } from "../swap/StepConnector.js";
import { WalletRow } from "../swap/WalletRow.js";
import type { PayerInfo } from "../types.js";
import { getProviderLabel } from "../utils.js";
import { StepContainer } from "./FiatSteps.js";
type OnRampScreenState = {
steps: Array<{
index: number;
step: OnRampStep;
status: FiatStatusMeta["progressStatus"];
}>;
handleContinue: () => void;
isLoading: boolean;
isDone: boolean;
isFailed: boolean;
};
export function OnRampScreen(props: {
title: string;
quote: BuyWithFiatQuote;
onBack: () => void;
client: ThirdwebClient;
testMode: boolean;
theme: "light" | "dark";
onDone: () => void;
transactionMode: boolean;
isEmbed: boolean;
payer: PayerInfo;
onSuccess: (status: BuyWithFiatStatus) => void;
receiverAddress: string;
paymentLinkId?: string;
}) {
const connectedWallets = useConnectedWallets();
const isAutoMode = isInAppSigner({
connectedWallets,
wallet: props.payer.wallet,
});
const state = useOnRampScreenState({
client: props.client,
isAutoMode,
onDone: props.onDone,
onSuccess: props.onSuccess,
payer: props.payer,
paymentLinkId: props.paymentLinkId,
quote: props.quote,
theme: props.theme,
});
const firstStepChainId = state.steps[0]?.step.token.chainId;
return (
<Container p="lg">
<ModalHeader onBack={props.onBack} title={props.title} />
<Spacer y="xl" />
<Container
center="y"
flex="column"
gap="xs"
style={{
paddingLeft: spacing.md,
}}
>
<WalletRow
address={props.receiverAddress}
client={props.client}
iconSize="md"
label="Recipient wallet"
textSize="sm"
/>
</Container>
<Spacer y="md" />
<Container flex="column">
{state.steps.map(({ step, status }, index) => (
<Container flex="column" key={step.action}>
<StepContainer
index={index}
state={status}
style={{
flex: "1",
}}
>
<StepUI
client={props.client}
index={index}
payer={props.payer}
step={step}
/>
</StepContainer>
{index < state.steps.length - 1 && <StepConnectorArrow />}
</Container>
))}
</Container>
<Spacer y="md" />
<Text
center
color="secondaryText"
size="xs"
style={{ padding: `0 ${spacing.xl}` }}
>
Keep this window open until all transactions are complete.
</Text>
<Spacer y="lg" />
<Container flex="column" gap="md">
{!state.isDone &&
firstStepChainId &&
firstStepChainId !== props.payer.chain.id ? (
<SwitchNetworkButton
fullWidth
switchChain={async () => {
await props.payer.wallet.switchChain(
getCachedChain(firstStepChainId),
);
}}
variant="accent"
/>
) : (
<Button
disabled={state.isLoading}
fullWidth
gap="sm"
onClick={state.handleContinue}
variant="accent"
>
{state.isLoading
? "Processing"
: state.isDone
? props.transactionMode
? "Continue Transaction"
: "Done"
: state.isFailed
? "Retry"
: "Continue"}
{state.isLoading && <Spinner color="primaryText" size="sm" />}
</Button>
)}
</Container>
</Container>
);
}
function StepUI(props: {
step: OnRampStep;
index: number;
client: ThirdwebClient;
payer: PayerInfo;
}) {
const { step, client } = props;
const chain = useChainName(getCachedChain(step.token.chainId));
return (
<Container flex="column" gap="xs" py="3xs">
<Container
center="y"
flex="row"
gap="sm"
style={{
display: "flex",
flexWrap: "nowrap",
justifyContent: "space-between",
}}
>
<PayTokenIcon
chain={getCachedChain(step.token.chainId)}
client={client}
size="md"
token={{
address: step.token.tokenAddress,
}}
/>
<Container center="y" flex="column" gap="3xs" style={{ flex: "1" }}>
<Text color="primaryText" size="sm">
{getProviderLabel(step.action)}
</Text>
<Container
center="y"
flex="row"
gap="xs"
style={{
display: "flex",
flexWrap: "nowrap",
justifyContent: "space-between",
}}
>
<Container
center="y"
flex="row"
gap="xxs"
style={{
flex: "1 1 60%",
flexWrap: "nowrap",
maxWidth: "60%",
minWidth: 0,
overflow: "hidden",
}}
>
<Text color="primaryText" size="sm">
{formatNumber(Number(step.amount), 5)}
</Text>
<TokenSymbol
chain={getCachedChain(step.token.chainId)}
color="secondaryText"
size="sm"
token={{
address: step.token.tokenAddress,
name: step.token.name || "",
symbol: step.token.symbol || "",
}}
/>
</Container>
<Container
center="y"
flex="row"
gap="xs"
style={{
flex: "1 1 40%",
flexWrap: "nowrap",
justifyContent: "flex-end",
maxWidth: "40%",
minWidth: 0,
}}
>
<Text
size="xs"
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{chain.name}
</Text>
</Container>
</Container>
</Container>
</Container>
</Container>
);
}
function useOnRampScreenState(props: {
quote: BuyWithFiatQuote;
client: ThirdwebClient;
onSuccess: (status: BuyWithFiatStatus) => void;
onDone: () => void;
payer: PayerInfo;
theme: "light" | "dark";
isAutoMode?: boolean;
paymentLinkId?: string;
}): OnRampScreenState {
const onRampSteps = getOnRampSteps(props.quote);
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [swapTxHash, setSwapTxHash] = useState<{
hash: string;
chainId: number;
}>();
const [popupWindow, setPopupWindow] = useState<Window | null>(null);
// Track onramp status
const { uiStatus: fiatOnrampStatus } = useOnRampStatus({
client: props.client,
intentId: props.quote.intentId,
onSuccess: (status) => {
if (onRampSteps.length === 1) {
// If only one step, this is the final success
props.onSuccess(status);
} else {
// Move to next step (swap)
setCurrentStepIndex((prev) => prev + 1);
}
},
openedWindow: popupWindow,
});
// Get quote for current swap/bridge step if needed
const previousStep = onRampSteps[currentStepIndex - 1];
const currentStep = onRampSteps[currentStepIndex];
// Handle swap execution
const swapMutation = useSwapMutation({
client: props.client,
isFiatFlow: true,
payer: props.payer,
paymentLinkId: props.paymentLinkId,
});
// Track swap status
const { uiStatus: swapStatus } = useSwapStatus({
chainId: swapTxHash?.chainId,
client: props.client,
onSuccess: () => {
if (currentStepIndex === onRampSteps.length - 1) {
// Last step completed - call final success
getBuyWithFiatStatus({
client: props.client,
intentId: props.quote.intentId,
}).then(props.onSuccess);
} else {
// Reset swap state before moving to next step
setSwapTxHash(undefined);
swapMutation.reset();
// Move to next step
setCurrentStepIndex((prev) => prev + 1);
}
},
transactionHash: swapTxHash?.hash,
});
// Map steps to their current status
const steps = onRampSteps.map((step, index) => {
let status: FiatStatusMeta["progressStatus"] = "unknown";
if (index === 0) {
// First step (onramp) status
status = fiatOnrampStatus;
} else if (index < currentStepIndex) {
// Previous steps are completed
status = "completed";
} else if (index === currentStepIndex) {
// Current step - could be swap or bridge
if (swapMutation.isPending) {
status = "pending";
} else if (swapMutation.error) {
status = "failed";
} else if (swapTxHash) {
status = swapStatus;
} else {
status = "actionRequired";
}
}
return {
index,
status,
step,
};
});
const isLoading = steps.some((step) => step.status === "pending");
const isDone = steps.every((step) => step.status === "completed");
const isFailed = steps.some((step) => step.status === "failed");
// Update handleContinue to handle done state
const handleContinue = useCallback(async () => {
if (isDone) {
props.onDone();
return;
}
if (currentStepIndex === 0) {
// First step - open onramp popup
const popup = openOnrampPopup(props.quote.onRampLink, props.theme);
trackPayEvent({
amountWei: props.quote.onRampToken.amountWei,
client: props.client,
event: "open_onramp_popup",
toChainId: props.quote.onRampToken.token.chainId,
toToken: props.quote.onRampToken.token.tokenAddress,
walletAddress: props.payer.account.address,
walletType: props.payer.wallet.id,
});
setPopupWindow(popup);
addPendingTx({
intentId: props.quote.intentId,
type: "fiat",
});
} else if (previousStep && currentStep && !swapTxHash) {
// Execute swap/bridge
try {
const result = await swapMutation.mutateAsync({
amount: currentStep.amount,
fromToken: previousStep.token,
toToken: currentStep.token,
});
setSwapTxHash({
chainId: result.chainId,
hash: result.transactionHash,
});
} catch (e) {
console.error("Failed to execute swap:", e);
}
} else if (isFailed) {
// retry the quote step
setSwapTxHash(undefined);
swapMutation.reset();
}
}, [
isDone,
currentStepIndex,
swapTxHash,
props.quote,
props.onDone,
swapMutation,
props.theme,
isFailed,
swapMutation.reset,
props.client,
props.payer.account.address,
props.payer.wallet.id,
currentStep,
previousStep,
]);
// Auto-progress effect
useEffect(() => {
if (!props.isAutoMode) {
return;
}
// Auto-start next swap step when previous step completes
if (
!isLoading &&
!isDone &&
!isFailed &&
currentStepIndex > 0 &&
currentStepIndex < onRampSteps.length &&
!swapTxHash
) {
handleContinue();
}
}, [
props.isAutoMode,
currentStepIndex,
swapTxHash,
onRampSteps.length,
handleContinue,
isDone,
isFailed,
isLoading,
]);
return {
handleContinue,
isDone,
isFailed,
isLoading,
steps,
};
}
function useOnRampStatus(props: {
intentId: string;
client: ThirdwebClient;
onSuccess: (status: BuyWithFiatStatus) => void;
openedWindow: Window | null;
}) {
const queryClient = useQueryClient();
const statusQuery = useBuyWithFiatStatus({
client: props.client,
intentId: props.intentId,
queryOptions: {
enabled: !!props.openedWindow,
},
});
let uiStatus: FiatStatusMeta["progressStatus"] = "actionRequired";
switch (statusQuery.data?.status) {
case "ON_RAMP_TRANSFER_COMPLETED":
uiStatus = "completed";
break;
case "PAYMENT_FAILED":
uiStatus = "failed";
break;
case "PENDING_PAYMENT":
uiStatus = "pending";
break;
default:
uiStatus = "actionRequired";
break;
}
const purchaseCbCalled = useRef(false);
useEffect(() => {
if (purchaseCbCalled.current || !props.onSuccess) {
return;
}
if (statusQuery.data && uiStatus === "completed") {
purchaseCbCalled.current = true;
props.onSuccess(statusQuery.data);
}
}, [props.onSuccess, statusQuery.data, uiStatus]);
// close the onramp popup if onramp is completed
useEffect(() => {
if (!props.openedWindow) {
return;
}
if (uiStatus === "completed") {
try {
if (props.openedWindow && !props.openedWindow.closed) {
props.openedWindow.close();
}
} catch (e) {
console.warn("Failed to close payment window:", e);
}
}
}, [props.openedWindow, uiStatus]);
// invalidate wallet balance when onramp is completed
const invalidatedBalance = useRef(false);
useEffect(() => {
if (!invalidatedBalance.current && uiStatus === "completed") {
invalidatedBalance.current = true;
invalidateWalletBalance(queryClient);
}
}, [uiStatus, queryClient]);
return { uiStatus };
}
function useSwapStatus(props: {
client: ThirdwebClient;
transactionHash?: string;
chainId?: number;
onSuccess: (status: BuyWithCryptoStatus) => void;
}) {
const swapStatus = useBuyWithCryptoStatus(
props.transactionHash && props.chainId
? {
chainId: props.chainId,
client: props.client,
transactionHash: props.transactionHash,
}
: undefined,
);
let uiStatus: FiatStatusMeta["progressStatus"] = "unknown";
switch (swapStatus.data?.status) {
case "COMPLETED":
uiStatus = "completed";
break;
case "FAILED":
uiStatus = "failed";
break;
case "PENDING":
case "NOT_FOUND":
uiStatus = "pending";
break;
case "NONE":
uiStatus = "unknown";
break;
default:
uiStatus = "unknown";
break;
}
const purchaseCbCalled = useRef(false);
useEffect(() => {
if (purchaseCbCalled.current || !props.onSuccess) {
return;
}
if (swapStatus.data?.status === "COMPLETED") {
purchaseCbCalled.current = true;
props.onSuccess(swapStatus.data);
}
}, [props.onSuccess, swapStatus]);
const queryClient = useQueryClient();
const balanceInvalidated = useRef(false);
useEffect(() => {
if (uiStatus === "completed" && !balanceInvalidated.current) {
balanceInvalidated.current = true;
invalidateWalletBalance(queryClient);
}
}, [queryClient, uiStatus]);
return { uiStatus };
}
function useSwapMutation(props: {
client: ThirdwebClient;
payer: PayerInfo;
isFiatFlow: boolean;
paymentLinkId?: string;
}) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: {
fromToken: PayTokenInfo;
toToken: PayTokenInfo;
amount: string;
}) => {
const { fromToken, toToken, amount } = input;
const wallet = props.payer.wallet;
// in case the wallet is not on the same chain as the fromToken, switch to it
if (wallet.getChain()?.id !== fromToken.chainId) {
await wallet.switchChain(getCachedChain(fromToken.chainId));
}
const account = wallet.getAccount();
if (!account) {
throw new Error("Payer wallet has no account");
}
// always get a fresh quote before executing
const quote = await getBuyWithCryptoQuote({
client: props.client,
fromAddress: account.address,
fromChainId: fromToken.chainId,
fromTokenAddress: fromToken.tokenAddress,
paymentLinkId: props.paymentLinkId,
toAddress: account.address,
toAmount: amount,
toChainId: toToken.chainId,
toTokenAddress: toToken.tokenAddress,
});
const canBatch = account.sendBatchTransaction;
const tokenContract = getContract({
address: quote.swapDetails.fromToken.tokenAddress,
chain: getCachedChain(quote.swapDetails.fromToken.chainId),
client: props.client,
});
const approveTxRequired =
quote.approvalData &&
(await allowance({
contract: tokenContract,
owner: account.address,
spender: quote.approvalData.spenderAddress,
})) < BigInt(quote.approvalData.amountWei);
if (approveTxRequired && quote.approvalData && !canBatch) {
trackPayEvent({
amountWei: quote.swapDetails.fromAmountWei,
chainId: quote.swapDetails.fromToken.chainId,
client: props.client,
event: "prompt_swap_approval",
fromToken: quote.swapDetails.fromToken.tokenAddress,
toChainId: quote.swapDetails.toToken.chainId,
toToken: quote.swapDetails.toToken.tokenAddress,
walletAddress: account.address,
walletType: props.payer.wallet.id,
});
const transaction = approve({
amountWei: BigInt(quote.approvalData.amountWei),
contract: tokenContract,
spender: quote.approvalData.spenderAddress,
});
const tx = await sendTransaction({
account,
transaction,
});
await waitForReceipt({ ...tx, maxBlocksWaitTime: 50 });
trackPayEvent({
amountWei: quote.swapDetails.fromAmountWei,
chainId: quote.swapDetails.fromToken.chainId,
client: props.client,
event: "swap_approval_success",
fromToken: quote.swapDetails.fromToken.tokenAddress,
toChainId: quote.swapDetails.toToken.chainId,
toToken: quote.swapDetails.toToken.tokenAddress,
walletAddress: account.address,
walletType: props.payer.wallet.id,
});
}
trackPayEvent({
amountWei: quote.swapDetails.fromAmountWei,
chainId: quote.swapDetails.fromToken.chainId,
client: props.client,
event: "prompt_swap_execution",
fromToken: quote.swapDetails.fromToken.tokenAddress,
toChainId: quote.swapDetails.toToken.chainId,
toToken: quote.swapDetails.toToken.tokenAddress,
walletAddress: account.address,
walletType: props.payer.wallet.id,
});
const tx = quote.transactionRequest;
let _swapTx: WaitForReceiptOptions;
// check if we can batch approval and swap
if (canBatch && quote.approvalData && approveTxRequired) {
const approveTx = approve({
amountWei: BigInt(quote.approvalData.amountWei),
contract: tokenContract,
spender: quote.approvalData.spenderAddress,
});
_swapTx = await sendBatchTransaction({
account,
transactions: [approveTx, tx],
});
} else {
_swapTx = await sendTransaction({
account,
transaction: tx,
});
}
await waitForReceipt({ ..._swapTx, maxBlocksWaitTime: 50 });
trackPayEvent({
amountWei: quote.swapDetails.fromAmountWei,
chainId: quote.swapDetails.fromToken.chainId,
client: props.client,
event: "swap_execution_success",
fromToken: quote.swapDetails.fromToken.tokenAddress,
toChainId: quote.swapDetails.toToken.chainId,
toToken: quote.swapDetails.toToken.tokenAddress,
walletAddress: account.address,
walletType: props.payer.wallet.id,
});
// do not add pending tx if the swap is part of fiat flow
if (!props.isFiatFlow) {
addPendingTx({
chainId: _swapTx.chain.id,
txHash: _swapTx.transactionHash,
type: "swap",
});
}
return {
chainId: _swapTx.chain.id,
transactionHash: _swapTx.transactionHash,
};
},
onSuccess: () => {
invalidateWalletBalance(queryClient);
},
});
}
@@ -1,54 +0,0 @@
import type { FiatProvider } from "../../../../../../../pay/utils/commonTypes.js";
import { Container } from "../../../../components/basic.js";
import { Button } from "../../../../components/buttons.js";
import { Link } from "../../../../components/text.js";
import { getProviderLabel } from "../utils.js";
/**
* @internal
*/
export function Providers(props: {
supportedProviders: FiatProvider[];
preferredProvider?: FiatProvider;
onSelect: (provider: FiatProvider) => void;
}) {
return (
<Container
expand
flex="column"
gap="sm"
style={{
alignItems: "flex-start",
}}
>
{props.supportedProviders.map((provider) => (
<Container
expand
flex="row"
key={provider}
style={{
justifyContent: "space-between",
}}
>
<Button
fullWidth
onClick={() => props.onSelect(provider)}
variant={"link"}
>
<Link
color={
props.preferredProvider === provider
? "primaryText"
: "secondaryText"
}
hoverColor="primaryText"
size="sm"
>
{getProviderLabel(provider)}
</Link>
</Button>
</Container>
))}
</Container>
);
}
@@ -84,7 +84,7 @@ export function getCurrencyMeta(shorthand: string): CurrencyMeta {
);
}
export function getFiatIcon(
function getFiatIcon(
currency: CurrencyMeta,
size: keyof typeof iconSize,
): React.ReactNode {
@@ -1,6 +1,4 @@
import type { ChainMetadata } from "../../../../../../../chains/types.js";
import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.js";
import type { GetWalletBalanceResult } from "../../../../../../../wallets/utils/getWalletBalance.js";
import type { TokenInfo } from "../../../../../../core/utils/defaultTokens.js";
@@ -12,40 +10,3 @@ export type TransactionCostAndData = {
gasCostWei: bigint;
chainMetadata: ChainMetadata;
};
export type SelectedScreen =
| {
id: "main" | "select-payment-method" | "buy-with-fiat";
}
| {
id: "buy-with-crypto";
payDisabled?: boolean;
}
| {
id: "select-from-token";
backScreen: SelectedScreen;
}
| {
id: "select-to-token";
backScreen: SelectedScreen;
}
| {
id: "select-currency";
backScreen: SelectedScreen;
}
| {
id: "swap-flow";
quote: BuyWithCryptoQuote;
approvalAmount?: bigint;
}
| {
id: "fiat-flow";
quote: BuyWithFiatQuote;
}
| {
id: "transfer-flow";
}
| {
id: "connect-payer-wallet";
backScreen: SelectedScreen;
};
@@ -1,65 +0,0 @@
import type { Chain } from "../../../../../../../chains/types.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js";
import type { SupportedChainAndTokens } from "../swap/useSwapSupportedChains.js";
// Based on what toToken, toChain, and supportedDestinations are, determine which payment methods should be enabled
// change the current method if it should be disabled
// return whether the payment selection should be shown or not ( if only one payment method is enabled, don't show the selection )
export type PaymentMethods = {
buyWithFiatEnabled: boolean;
buyWithCryptoEnabled: boolean;
};
export function useEnabledPaymentMethods(options: {
payOptions: PayUIOptions;
supportedDestinations: SupportedChainAndTokens;
toChain: Chain;
toToken: ERC20OrNativeToken;
}): PaymentMethods {
const { payOptions, supportedDestinations, toChain, toToken } = options;
function getEnabledPayMethodsForSelectedToken(): {
fiat: boolean;
swap: boolean;
} {
const chain = supportedDestinations.find((c) => c.chain.id === toChain.id);
if (!chain) {
return {
fiat: false,
swap: false,
};
}
const toTokenAddress = isNativeToken(toToken)
? NATIVE_TOKEN_ADDRESS
: toToken.address;
const tokenInfo = chain.tokens.find(
(t) => t.address.toLowerCase() === toTokenAddress.toLowerCase(),
);
if (!tokenInfo) {
return {
fiat: true,
swap: true,
};
}
return {
fiat: tokenInfo.buyWithFiatEnabled,
swap: tokenInfo.buyWithCryptoEnabled,
};
}
const { fiat, swap } = getEnabledPayMethodsForSelectedToken();
const buyWithFiatEnabled = payOptions.buyWithFiat !== false && fiat;
const buyWithCryptoEnabled = payOptions.buyWithCrypto !== false && swap;
return {
buyWithCryptoEnabled,
buyWithFiatEnabled,
};
}
@@ -1,193 +0,0 @@
import { useEffect, useState } from "react";
import { polygon } from "../../../../../../../chains/chain-definitions/polygon.js";
import type { Chain } from "../../../../../../../chains/types.js";
import type {
FundWalletOptions,
PayUIOptions,
} from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
import { useActiveWalletChain } from "../../../../../../core/hooks/wallets/useActiveWalletChain.js";
import type { TokenInfo } from "../../../../../../core/utils/defaultTokens.js";
import { useDebouncedValue } from "../../../../hooks/useDebouncedValue.js";
import { type ERC20OrNativeToken, NATIVE_TOKEN } from "../../nativeToken.js";
import {
type CurrencyMeta,
currencies,
usdCurrency,
} from "../fiat/currencies.js";
import type { SupportedChainAndTokens } from "../swap/useSwapSupportedChains.js";
type SupportedSourcesInputData = {
chain: Chain;
tokens: {
address: string;
buyWithCryptoEnabled: boolean;
buyWithFiatEnabled: boolean;
name: string;
symbol: string;
}[];
};
// handle states for token and chain selection
export function useToTokenSelectionStates(options: {
payOptions: PayUIOptions;
supportedDestinations: SupportedChainAndTokens;
}) {
const { payOptions, supportedDestinations } = options;
// --------------------------------------------------------------------------
// buy token amount ---------------------------------------------------------
// NOTE - for transaction / direct payment modes, the token amount is set when the user tap continue
const prefillBuy = (payOptions as FundWalletOptions)?.prefillBuy;
const activeChain = useActiveWalletChain();
const initialTokenAmount = prefillBuy?.amount || "";
const [tokenAmount, setTokenAmount] = useState<string>(initialTokenAmount);
const deferredTokenAmount = useDebouncedValue(tokenAmount, 300);
useEffect(() => {
if (prefillBuy?.amount) {
setTokenAmount(prefillBuy.amount);
}
if (prefillBuy?.chain) {
setToChain(prefillBuy.chain);
}
if (prefillBuy?.token) {
setToToken(prefillBuy.token as TokenInfo);
}
}, [prefillBuy?.amount, prefillBuy?.chain, prefillBuy?.token]);
// Destination chain and token selection -----------------------------------
const [toChain, setToChain] = useState<Chain>(
// use prefill chain if available
prefillBuy?.chain ||
(payOptions.mode === "transaction" && payOptions.transaction?.chain) ||
(payOptions.mode === "direct_payment" && payOptions.paymentInfo?.chain) ||
// use active chain if its supported as destination
supportedDestinations.find((x) => x.chain.id === activeChain?.id)
?.chain ||
// default to the first chain in supportedDestinations, or polygon if nothing is found at all
supportedDestinations[0]?.chain ||
polygon,
);
const [toToken, setToToken] = useState<ERC20OrNativeToken>(
(prefillBuy?.token as TokenInfo) ||
(payOptions.mode === "direct_payment" && payOptions.paymentInfo.token) ||
NATIVE_TOKEN,
);
return {
deferredTokenAmount,
setToChain,
setTokenAmount,
setToToken,
toChain,
tokenAmount,
toToken,
};
}
export function useFromTokenSelectionStates(options: {
payOptions: PayUIOptions;
supportedSources: SupportedSourcesInputData[];
}) {
const { payOptions } = options;
// TODO (pay) - auto select token based on connected wallet balances
// Source token and chain selection ---------------------------------------------------
const [fromChain_, setFromChain] = useState<Chain>();
// use prefill chain if available
const fromChainDevSpecified =
(payOptions.buyWithCrypto !== false &&
payOptions.buyWithCrypto?.prefillSource?.chain) ||
(payOptions.mode === "transaction" && payOptions.transaction?.chain) ||
(payOptions.mode === "direct_payment" && payOptions.paymentInfo?.chain);
const fromChain = fromChain_ || fromChainDevSpecified || undefined;
const [fromToken_, setFromToken] = useState<ERC20OrNativeToken>();
// use prefill token if available
const fromTokenDevSpecified =
(payOptions.buyWithCrypto !== false &&
payOptions.buyWithCrypto?.prefillSource?.token) ||
(payOptions.mode === "direct_payment" && payOptions.paymentInfo.token);
// supported tokens query in here
const fromToken = fromToken_ || fromTokenDevSpecified || undefined;
return {
fromChain,
fromToken,
setFromChain,
setFromToken,
};
}
export function useFiatCurrencySelectionStates(options: {
payOptions: PayUIOptions;
}) {
const { payOptions } = options;
// --------------------------------------------------------------------------
const devSpecifiedDefaultCurrency =
payOptions.buyWithFiat !== false
? payOptions.buyWithFiat?.prefillSource?.currency
: undefined;
const defaultSelectedCurrencyShorthand =
devSpecifiedDefaultCurrency || getDefaultCurrencyBasedOnLocation();
const [selectedCurrency, setSelectedCurrency] = useState(
currencies.find((x) => x.shorthand === defaultSelectedCurrencyShorthand) ||
usdCurrency,
);
return {
selectedCurrency,
setSelectedCurrency,
};
}
function getDefaultCurrencyBasedOnLocation(): CurrencyMeta["shorthand"] {
// if Intl is not supported - browser throws
try {
const timeZone = Intl.DateTimeFormat()
.resolvedOptions()
.timeZone.toLowerCase();
// Europe/London -> GBP
if (timeZone.includes("london")) {
return "GBP";
}
// Europe/* -> EUR
if (timeZone.includes("europe")) {
return "EUR";
}
// Japan
if (timeZone.includes("japan")) {
return "JPY";
}
// canada
if (timeZone.includes("canada")) {
return "CAD";
}
// australia
if (timeZone.includes("australia")) {
return "AUD";
}
// new zealand
if (timeZone.includes("new zealand")) {
return "NZD";
}
return "USD";
} catch {
return "USD";
}
}
@@ -1,12 +0,0 @@
export function openOnrampPopup(link: string, theme: string) {
const height = 750;
const width = 500;
const top = (window.innerHeight - height) / 2;
const left = (window.innerWidth - width) / 2;
return window.open(
`${link}&theme=${theme}`,
"thirdweb Pay",
`width=${width}, height=${height}, top=${top}, left=${left}`,
);
}
@@ -1,294 +0,0 @@
import { ExternalLinkIcon } from "@radix-ui/react-icons";
import { getCachedChain } from "../../../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js";
import type { ValidBuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import { formatExplorerTxUrl } from "../../../../../../../utils/url.js";
import {
fontSize,
iconSize,
spacing,
} from "../../../../../../core/design-system/index.js";
import {
useChainExplorers,
useChainName,
} from "../../../../../../core/hooks/others/useChainQuery.js";
import { Container, Line } from "../../../../components/basic.js";
import { ButtonLink } from "../../../../components/buttons.js";
import { Spacer } from "../../../../components/Spacer.js";
import { Text } from "../../../../components/text.js";
import { WalletRow } from "../swap/WalletRow.js";
import { getBuyWithCryptoStatusMeta, type StatusMeta } from "./statusMeta.js";
import { TokenInfoRow } from "./TokenInfoRow.js";
type SwapTxDetailsData = {
fromToken: {
chainId: number;
symbol: string;
address: string;
amount: string;
};
quotedToToken: {
chainId: number;
symbol: string;
address: string;
amount: string;
};
gotToken?: {
chainId: number;
symbol: string;
address: string;
amount: string;
};
statusMeta?: StatusMeta;
sourceTxHash?: string;
destinationTxHash?: string;
isPartialSuccess: boolean;
estimatedDuration: number;
fromAddress: string;
toAddress: string;
};
export function SwapTxDetailsTable(
props:
| {
type: "quote";
quote: BuyWithCryptoQuote;
client: ThirdwebClient;
}
| {
client: ThirdwebClient;
type: "status";
status: ValidBuyWithCryptoStatus;
hideStatusRow?: boolean;
},
) {
let uiData: SwapTxDetailsData;
let showStatusRow = true;
if (props.type === "status") {
const status = props.status;
if (props.hideStatusRow) {
showStatusRow = false;
}
const isPartialSuccess =
status.status === "COMPLETED" && status.subStatus === "PARTIAL_SUCCESS";
uiData = {
destinationTxHash: status.destination?.transactionHash,
estimatedDuration: status.quote.estimated.durationSeconds || 0,
fromAddress: status.fromAddress,
fromToken: {
address: status.quote.fromToken.tokenAddress,
amount: status.quote.fromAmount,
chainId: status.quote.fromToken.chainId,
symbol: status.quote.fromToken.symbol || "",
},
gotToken: status.destination
? {
address: status.destination.token.tokenAddress,
amount: status.destination.amount,
chainId: status.destination.token.chainId,
symbol: status.destination.token.symbol || "",
}
: undefined,
isPartialSuccess,
quotedToToken: {
address: status.quote.toToken.tokenAddress,
amount: status.quote.toAmount,
chainId: status.quote.toToken.chainId,
symbol: status.quote.toToken.symbol || "",
},
sourceTxHash: status.source?.transactionHash,
statusMeta: getBuyWithCryptoStatusMeta(status),
toAddress: status.toAddress,
};
} else {
const quote = props.quote;
uiData = {
estimatedDuration: quote.swapDetails.estimated.durationSeconds || 0,
fromAddress: quote.swapDetails.fromAddress,
fromToken: {
address: quote.swapDetails.fromToken.tokenAddress,
amount: quote.swapDetails.fromAmount,
chainId: quote.swapDetails.fromToken.chainId,
symbol: quote.swapDetails.fromToken.symbol || "",
},
isPartialSuccess: false,
quotedToToken: {
address: quote.swapDetails.toToken.tokenAddress,
amount: quote.swapDetails.toAmount,
chainId: quote.swapDetails.toToken.chainId,
symbol: quote.swapDetails.toToken.symbol || "",
},
toAddress: quote.swapDetails.toAddress,
};
}
const { client } = props;
const {
fromToken,
quotedToToken: toToken,
statusMeta,
sourceTxHash,
destinationTxHash,
isPartialSuccess,
gotToken,
} = uiData;
const fromChainId = fromToken.chainId;
const toChainId = toToken.chainId;
const fromChainName = useChainName(getCachedChain(fromChainId));
const fromChainExplorers = useChainExplorers(getCachedChain(fromChainId));
const toChainName = useChainName(getCachedChain(toChainId));
const toChainExplorers = useChainExplorers(getCachedChain(toChainId));
const lineSpacer = (
<>
<Spacer y="md" />
<Line />
<Spacer y="md" />
</>
);
return (
<div>
{/* Pay */}
<TokenInfoRow
chainId={fromToken.chainId}
client={client}
label="Paid"
tokenAddress={fromToken.address}
tokenAmount={fromToken.amount}
tokenSymbol={fromToken.symbol || ""}
/>
{lineSpacer}
{isPartialSuccess && gotToken ? (
// Expected + Got
<>
{/* Expected */}
<TokenInfoRow
chainId={toToken.chainId}
client={client}
label={isPartialSuccess ? "Expected" : "Received"}
tokenAddress={toToken.address}
tokenAmount={toToken.amount}
tokenSymbol={toToken.symbol || ""}
/>
{lineSpacer}
{/* Got */}
<TokenInfoRow
chainId={gotToken.chainId}
client={client}
label="Got"
tokenAddress={gotToken.address}
tokenAmount={gotToken.amount}
tokenSymbol={gotToken.symbol || ""}
/>
</>
) : (
// Receive
<TokenInfoRow
chainId={toToken.chainId}
client={client}
label="Received"
tokenAddress={toToken.address}
tokenAmount={toToken.amount}
tokenSymbol={toToken.symbol || ""}
/>
)}
{lineSpacer}
<Container
flex="row"
style={{
justifyContent: "space-between",
}}
>
<Text size="sm">Recipient</Text>
<WalletRow address={uiData.toAddress} client={client} iconSize="sm" />
</Container>
{/* Status */}
{statusMeta && showStatusRow && (
<>
{lineSpacer}
<Container
flex="row"
style={{
justifyContent: "space-between",
}}
>
<Text size="sm">Status</Text>
<Container
center="y"
flex="column"
gap="3xs"
style={{
alignItems: "flex-end",
}}
>
<Text color={statusMeta.color} size="sm">
{statusMeta.status}
</Text>
</Container>
</Container>
</>
)}
<Spacer y="lg" />
{/* source chain Tx hash link */}
{fromChainExplorers.explorers?.[0]?.url && sourceTxHash && (
<ButtonLink
fullWidth
gap="xs"
href={formatExplorerTxUrl(
fromChainExplorers.explorers[0]?.url,
sourceTxHash,
)}
style={{
fontSize: fontSize.sm,
padding: spacing.sm,
}}
target="_blank"
variant="outline"
>
View on {fromChainName.name} Explorer
<ExternalLinkIcon height={iconSize.sm} width={iconSize.sm} />
</ButtonLink>
)}
{/* destination chain tx hash link */}
{destinationTxHash &&
sourceTxHash !== destinationTxHash &&
toChainExplorers?.explorers?.[0]?.url && (
<>
<Spacer y="sm" />
<ButtonLink
fullWidth
gap="xs"
href={formatExplorerTxUrl(
toChainExplorers.explorers[0]?.url,
destinationTxHash,
)}
style={{
fontSize: fontSize.sm,
padding: spacing.sm,
}}
target="_blank"
variant="outline"
>
View on {toChainName.name} Explorer
<ExternalLinkIcon height={iconSize.sm} width={iconSize.sm} />
</ButtonLink>
</>
)}
</div>
);
}
@@ -1,56 +0,0 @@
import { useMemo } from "react";
import { getCachedChain } from "../../../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
import { formatNumber } from "../../../../../../../utils/formatNumber.js";
import { useChainName } from "../../../../../../core/hooks/others/useChainQuery.js";
import { Container } from "../../../../components/basic.js";
import { Text } from "../../../../components/text.js";
import { PayTokenIcon } from "../PayTokenIcon.js";
export function TokenInfoRow(props: {
tokenSymbol: string;
tokenAmount: string;
tokenAddress: string;
chainId: number;
label: string;
client: ThirdwebClient;
}) {
const chainObj = useMemo(
() => getCachedChain(props.chainId),
[props.chainId],
);
const { name } = useChainName(chainObj);
return (
<Container
flex="row"
style={{
justifyContent: "space-between",
}}
>
<Text size="sm">{props.label}</Text>
<Container
flex="column"
gap="3xs"
style={{
alignItems: "flex-end",
}}
>
<Container center="y" flex="row" gap="xs">
<PayTokenIcon
chain={chainObj}
client={props.client}
size="sm"
token={{
address: props.tokenAddress,
}}
/>
<Text color="primaryText" size="sm">
{formatNumber(Number(props.tokenAmount), 6)} {props.tokenSymbol}
</Text>
</Container>
<Text size="xs">{name}</Text>
</Container>
</Container>
);
}
@@ -1,80 +0,0 @@
import { describe, expect, it } from "vitest";
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import { getBuyWithCryptoStatusMeta } from "./statusMeta.js";
describe("getBuyWithCryptoStatusMeta", () => {
it('returns "Unknown" for NOT_FOUND status', () => {
const result = getBuyWithCryptoStatusMeta({
status: "NOT_FOUND",
} as BuyWithCryptoStatus);
expect(result).toEqual({
color: "secondaryText",
status: "Unknown",
});
});
it('returns "Bridging" for WAITING_BRIDGE subStatus', () => {
const result = getBuyWithCryptoStatusMeta({
status: "PENDING",
subStatus: "WAITING_BRIDGE",
} as BuyWithCryptoStatus);
expect(result).toEqual({
color: "accentText",
loading: true,
status: "Bridging",
});
});
it('returns "Incomplete" for PARTIAL_SUCCESS subStatus', () => {
const result = getBuyWithCryptoStatusMeta({
status: "COMPLETED",
subStatus: "PARTIAL_SUCCESS",
} as BuyWithCryptoStatus);
expect(result).toEqual({
color: "secondaryText",
status: "Incomplete",
});
});
it('returns "Pending" for PENDING status', () => {
const result = getBuyWithCryptoStatusMeta({
status: "PENDING",
} as BuyWithCryptoStatus);
expect(result).toEqual({
color: "accentText",
loading: true,
status: "Pending",
});
});
it('returns "Failed" for FAILED status', () => {
const result = getBuyWithCryptoStatusMeta({
status: "FAILED",
} as BuyWithCryptoStatus);
expect(result).toEqual({
color: "danger",
status: "Failed",
});
});
it('returns "Completed" for COMPLETED status', () => {
const result = getBuyWithCryptoStatusMeta({
status: "COMPLETED",
} as BuyWithCryptoStatus);
expect(result).toEqual({
color: "success",
status: "Completed",
});
});
it('returns "Unknown" for unhandled status', () => {
const result = getBuyWithCryptoStatusMeta({
// @ts-ignore Test purpose
status: "Unknown",
});
expect(result).toEqual({
color: "secondaryText",
status: "Unknown",
});
});
});
@@ -1,77 +0,0 @@
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import type { Theme } from "../../../../../../core/design-system/index.js";
export type StatusMeta = {
status: string;
color: keyof Theme["colors"];
loading?: true;
};
export function getBuyWithCryptoStatusMeta(
cryptoStatus: BuyWithCryptoStatus,
): StatusMeta {
if (cryptoStatus.status === "NOT_FOUND") {
return {
color: "secondaryText",
status: "Unknown",
};
}
const subStatus = cryptoStatus.subStatus;
const status = cryptoStatus.status;
if (subStatus === "WAITING_BRIDGE") {
return {
color: "accentText",
loading: true,
status: "Bridging",
};
}
if (subStatus === "PARTIAL_SUCCESS") {
return {
color: "secondaryText",
status: "Incomplete",
};
}
if (status === "PENDING") {
return {
color: "accentText",
loading: true,
status: "Pending",
};
}
if (status === "FAILED") {
return {
color: "danger",
status: "Failed",
};
}
if (status === "COMPLETED") {
return {
color: "success",
status: "Completed",
};
}
return {
color: "secondaryText",
status: "Unknown",
};
}
export type FiatStatusMeta = {
status: string;
color: keyof Theme["colors"];
loading?: true;
step: 1 | 2;
progressStatus:
| "pending"
| "completed"
| "failed"
| "actionRequired"
| "unknown";
};

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