Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c6dd874e0 | ||
|
|
a9e47e4759 | ||
|
|
bfd728a9d1 | ||
|
|
381a2823d7 | ||
|
|
2111b8b12c | ||
|
|
1665e4d534 | ||
|
|
692cedf921 | ||
|
|
e1f4901b80 | ||
|
|
028f871d40 | ||
|
|
2821b4eeb7 | ||
|
|
adfa56b2a2 | ||
|
|
415ad90986 | ||
|
|
afcb7bba84 | ||
|
|
10c3b6f27f | ||
|
|
c64446306e | ||
|
|
2136c7ba4a | ||
|
|
67c3d861fe | ||
|
|
d2c12cc6ef | ||
|
|
c1b2e10f34 | ||
|
|
f48d8006bc | ||
|
|
20e323d666 |
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
+17
-13
@@ -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"
|
||||
/>
|
||||
|
||||
+18
-16
@@ -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 = {
|
||||
|
||||
+1
-1
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
/>
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+115
@@ -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>
|
||||
);
|
||||
}
|
||||
+16
@@ -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)) {
|
||||
|
||||
+16
-10
@@ -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;
|
||||
|
||||
+3
@@ -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>
|
||||
|
||||
+12
-5
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
+56
-1
@@ -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(
|
||||
|
||||
+58
-6
@@ -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>
|
||||
|
||||
+10
-1
@@ -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",
|
||||
|
||||
+6
-4
@@ -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>({
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+9
-2
@@ -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) => `\
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-3
@@ -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>
|
||||
|
||||
+11
-3
@@ -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) {
|
||||
|
||||
+6
-2
@@ -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>
|
||||
|
||||
+1
-1
@@ -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`,
|
||||
},
|
||||
]}
|
||||
|
||||
+1
-1
@@ -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.{" "}
|
||||
|
||||
+102
-69
@@ -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>;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -4,8 +4,8 @@
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on",
|
||||
"useSortedAttributes": "on",
|
||||
"useSortedKeys": "on"
|
||||
"useSortedAttributes": "off",
|
||||
"useSortedKeys": "off"
|
||||
}
|
||||
},
|
||||
"enabled": true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -88,14 +88,6 @@ export type ChainMetadata = {
|
||||
stackType: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @chain
|
||||
*/
|
||||
export type ChainService = {
|
||||
service: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @chain
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
-286
@@ -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",
|
||||
};
|
||||
});
|
||||
-395
@@ -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>
|
||||
);
|
||||
}
|
||||
-61
@@ -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",
|
||||
};
|
||||
});
|
||||
-263
@@ -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;
|
||||
};
|
||||
|
||||
-65
@@ -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,
|
||||
};
|
||||
}
|
||||
-193
@@ -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}`,
|
||||
);
|
||||
}
|
||||
-294
@@ -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>
|
||||
);
|
||||
}
|
||||
-56
@@ -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>
|
||||
);
|
||||
}
|
||||
-80
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
-77
@@ -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
Reference in New Issue
Block a user