Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31e87b07a8 | ||
|
|
f2d0f119c1 | ||
|
|
dc07fdd952 | ||
|
|
1d598ce907 | ||
|
|
f157e3484a | ||
|
|
d95b3d38c7 | ||
|
|
b50b320144 | ||
|
|
4c339363c0 | ||
|
|
125cc972de | ||
|
|
436750ed15 |
@@ -4,6 +4,11 @@ export const DASHBOARD_THIRDWEB_CLIENT_ID =
|
||||
export const DASHBOARD_THIRDWEB_SECRET_KEY =
|
||||
process.env.DASHBOARD_SECRET_KEY || "";
|
||||
|
||||
export const NEXT_PUBLIC_NEBULA_APP_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_NEBULA_APP_CLIENT_ID || "";
|
||||
|
||||
export const NEBULA_APP_SECRET_KEY = process.env.NEBULA_APP_SECRET_KEY || "";
|
||||
|
||||
export const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
|
||||
|
||||
export const IPFS_GATEWAY_URL =
|
||||
|
||||
+1
@@ -60,6 +60,7 @@ export default async function Page(props: {
|
||||
trackingCategory="account-abstraction-project-settings"
|
||||
project={project}
|
||||
teamId={team.id}
|
||||
teamSlug={team.slug}
|
||||
validTeamPlan={getValidTeamPlan(team)}
|
||||
/>
|
||||
</ChakraProviderSetup>
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export default async function Page(props: {
|
||||
teamId={team.id}
|
||||
trackingCategory="in-app-wallet-project-settings"
|
||||
teamSlug={team_slug}
|
||||
validTeamPlan={getValidTeamPlan(team)}
|
||||
teamPlan={getValidTeamPlan(team)}
|
||||
smsCountryTiers={smsCountryTiers}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -29,12 +29,15 @@ import {
|
||||
AccountBlobbie,
|
||||
AccountName,
|
||||
AccountProvider,
|
||||
WalletName,
|
||||
WalletProvider,
|
||||
} from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
import type { NebulaContext } from "../api/chat";
|
||||
|
||||
export type WalletMeta = {
|
||||
type: "user" | "smart";
|
||||
walletId: Wallet["id"];
|
||||
address: string;
|
||||
};
|
||||
|
||||
@@ -95,7 +98,7 @@ export function ChatBar(props: {
|
||||
hideTestnets
|
||||
disableChainId
|
||||
selectedChainIds={selectedChainIds}
|
||||
popoverContentClassName="!w-[calc(100vw-80px)] lg:!w-[360px]"
|
||||
popoverContentClassName="!w-[calc(100vw-80px)] lg:!w-[320px]"
|
||||
align="start"
|
||||
side="top"
|
||||
showSelectedValuesInModal={true}
|
||||
@@ -257,8 +260,22 @@ function WalletSelector(props: {
|
||||
aria-expanded={open}
|
||||
className="flex h-auto items-center gap-1 rounded-full px-2 py-1.5 text-xs"
|
||||
>
|
||||
{shortenAddress(props.activeAccountAddress)}
|
||||
<ChevronDown className="ml-1 size-3 text-muted-foreground/70" />
|
||||
<AccountProvider
|
||||
address={props.activeAccountAddress}
|
||||
client={props.client}
|
||||
>
|
||||
<AccountAvatar
|
||||
className="size-4 rounded-full"
|
||||
loadingComponent={
|
||||
<Skeleton className="size-3 rounded-full border" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="size-3 rounded-full" />
|
||||
}
|
||||
/>
|
||||
{shortenAddress(props.activeAccountAddress)}
|
||||
<ChevronDown className="size-3 text-muted-foreground/70" />
|
||||
</AccountProvider>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -288,48 +305,58 @@ function WalletSelector(props: {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<AccountProvider address={wallet.address} client={props.client}>
|
||||
<div className="flex items-center gap-2">
|
||||
<AccountAvatar
|
||||
className="size-8 rounded-full"
|
||||
loadingComponent={
|
||||
<Skeleton className="size-8 rounded-full border" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="size-8 rounded-full" />
|
||||
}
|
||||
/>
|
||||
<WalletProvider id={wallet.walletId}>
|
||||
<div className="flex items-center gap-2">
|
||||
<AccountAvatar
|
||||
className="size-8 rounded-full"
|
||||
loadingComponent={
|
||||
<Skeleton className="size-8 rounded-full border" />
|
||||
}
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="size-8 rounded-full" />
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<AccountName
|
||||
className="text-sm"
|
||||
loadingComponent={
|
||||
<span className="font-mono text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="font-mono text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<AccountName
|
||||
className="text-sm"
|
||||
loadingComponent={
|
||||
<span className="font-mono text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
fallbackComponent={
|
||||
<span className="font-mono text-sm">
|
||||
{shortenAddress(wallet.address)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
<CopyButton address={wallet.address} />
|
||||
<CopyButton address={wallet.address} />
|
||||
|
||||
{wallet.type === "smart" && (
|
||||
<Badge variant="outline" className="bg-card px-2">
|
||||
Gasless
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{wallet.walletId === "smart" && (
|
||||
<Badge variant="outline" className="bg-card px-2">
|
||||
Gasless
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{wallet.type === "user" && "Your Account"}
|
||||
{wallet.type === "smart" && "Smart Account"}
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{wallet.walletId === "smart" ? (
|
||||
"Smart Account"
|
||||
) : (
|
||||
<WalletName
|
||||
fallbackComponent={<span> Your Account </span>}
|
||||
loadingComponent={
|
||||
<Skeleton className="h-3.5 w-40" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WalletProvider>
|
||||
</AccountProvider>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ export function ChatPageContent(props: {
|
||||
|
||||
const connectedWalletsMeta: WalletMeta[] = connectedWallets.map((x) => ({
|
||||
address: x.getAccount()?.address || "",
|
||||
type: x.id === "smart" ? "smart" : "user",
|
||||
walletId: x.id,
|
||||
}));
|
||||
|
||||
const handleUpdateContextFilters = async (
|
||||
@@ -491,6 +491,8 @@ export async function handleNebulaPrompt(params: {
|
||||
} = params;
|
||||
let requestIdForMessage = "";
|
||||
|
||||
let hasReceivedResponse = false;
|
||||
|
||||
await promptNebula({
|
||||
abortController,
|
||||
message,
|
||||
@@ -506,6 +508,12 @@ export async function handleNebulaPrompt(params: {
|
||||
}
|
||||
|
||||
if (res.event === "delta") {
|
||||
// ignore empty string delta
|
||||
if (!res.data.v) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasReceivedResponse = true;
|
||||
setMessages((prev) => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
// if last message is presence, overwrite it
|
||||
@@ -561,6 +569,7 @@ export async function handleNebulaPrompt(params: {
|
||||
|
||||
if (res.event === "action") {
|
||||
if (res.type === "sign_transaction") {
|
||||
hasReceivedResponse = true;
|
||||
setMessages((prev) => {
|
||||
let prevMessages = prev;
|
||||
// if last message is presence, remove it
|
||||
@@ -588,6 +597,24 @@ export async function handleNebulaPrompt(params: {
|
||||
},
|
||||
context: contextFilters,
|
||||
});
|
||||
|
||||
// if the stream ends without any delta or tx events - we have nothing to show
|
||||
// show an error message in that case
|
||||
if (!hasReceivedResponse) {
|
||||
setMessages((prev) => {
|
||||
const newMessages = prev.slice(
|
||||
0,
|
||||
prev[prev.length - 1]?.type === "presence" ? -1 : undefined,
|
||||
);
|
||||
|
||||
newMessages.push({
|
||||
text: "No response received, please try again",
|
||||
type: "error",
|
||||
});
|
||||
|
||||
return newMessages;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleNebulaPromptError(params: {
|
||||
|
||||
@@ -40,19 +40,23 @@ export function ChatSidebar(props: {
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-start gap-3 p-4 lg:justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<NebulaIcon className="size-8 text-foreground" aria-label="Nebula" />
|
||||
<NebulaIcon className="size-6 text-foreground" aria-label="Nebula" />
|
||||
<span className="font-semibold text-lg tracking-tight">Nebula</span>
|
||||
</Link>
|
||||
|
||||
<Badge variant="secondary" className="gap-1 py-1">
|
||||
Beta
|
||||
Alpha
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="h-1" />
|
||||
|
||||
<div className="flex flex-col gap-2 px-4">
|
||||
<Button asChild variant="outline" className="w-full gap-2 rounded-lg">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full gap-2 rounded-lg bg-muted/50"
|
||||
>
|
||||
<Link href={newChatPage}>
|
||||
<PlusIcon className="size-4" />
|
||||
New Chat
|
||||
|
||||
@@ -77,7 +77,7 @@ function Story() {
|
||||
showContextSelector={true}
|
||||
connectedWallets={[
|
||||
{
|
||||
type: "user",
|
||||
walletId: "io.metamask",
|
||||
address: userWalletAddress,
|
||||
},
|
||||
]}
|
||||
@@ -94,11 +94,11 @@ function Story() {
|
||||
showContextSelector={true}
|
||||
connectedWallets={[
|
||||
{
|
||||
type: "user",
|
||||
walletId: "io.metamask",
|
||||
address: userWalletAddress,
|
||||
},
|
||||
{
|
||||
type: "smart",
|
||||
walletId: "smart",
|
||||
address: smartWalletAddress,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ArrowUpRightIcon } from "lucide-react";
|
||||
import type { NebulaContext } from "../api/chat";
|
||||
import { examplePrompts } from "../data/examplePrompts";
|
||||
import { NebulaIcon } from "../icons/NebulaIcon";
|
||||
import { nebulaThirdwebClient } from "../utils/nebulaThirdwebClient";
|
||||
import { nebulaAppThirdwebClient } from "../utils/nebulaThirdwebClient";
|
||||
import { ChatBar, type WalletMeta } from "./ChatBar";
|
||||
|
||||
export function EmptyStateChatPageContent(props: {
|
||||
@@ -42,7 +42,7 @@ export function EmptyStateChatPageContent(props: {
|
||||
setContext={props.setContext}
|
||||
sendMessage={props.sendMessage}
|
||||
isChatStreaming={false}
|
||||
client={nebulaThirdwebClient}
|
||||
client={nebulaAppThirdwebClient}
|
||||
connectedWallets={props.connectedWallets}
|
||||
activeAccountAddress={props.activeAccountAddress}
|
||||
setActiveWallet={props.setActiveWallet}
|
||||
|
||||
+7
-3
@@ -5,7 +5,10 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { useActiveWalletConnectionStatus } from "thirdweb/react";
|
||||
import {
|
||||
useActiveWallet,
|
||||
useActiveWalletConnectionStatus,
|
||||
} from "thirdweb/react";
|
||||
import type { NebulaContext } from "../../api/chat";
|
||||
import { createSession } from "../../api/session";
|
||||
import type { ExamplePrompt } from "../../data/examplePrompts";
|
||||
@@ -69,6 +72,7 @@ function FloatingChatContentLoggedIn(props: {
|
||||
const [isChatStreaming, setIsChatStreaming] = useState(false);
|
||||
const [enableAutoScroll, setEnableAutoScroll] = useState(false);
|
||||
const connectionStatus = useActiveWalletConnectionStatus();
|
||||
const activeWallet = useActiveWallet();
|
||||
|
||||
const [contextFilters, setContextFilters] = useState<
|
||||
NebulaContext | undefined
|
||||
@@ -194,11 +198,11 @@ function FloatingChatContentLoggedIn(props: {
|
||||
setContext={setContextFilters}
|
||||
showContextSelector={false}
|
||||
connectedWallets={
|
||||
props.nebulaParams?.wallet
|
||||
props.nebulaParams?.wallet && activeWallet
|
||||
? [
|
||||
{
|
||||
address: props.nebulaParams.wallet,
|
||||
type: "user",
|
||||
walletId: activeWallet.id,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSDKTheme } from "app/(app)/components/sdk-component-theme";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTheme } from "next-themes";
|
||||
@@ -14,15 +14,14 @@ import {
|
||||
useActiveAccount,
|
||||
useActiveWalletConnectionStatus,
|
||||
} from "thirdweb/react";
|
||||
import { cn } from "../../../../@/lib/utils";
|
||||
import { doNebulaLogout } from "../../login/auth-actions";
|
||||
import { nebulaAppThirdwebClient } from "../utils/nebulaThirdwebClient";
|
||||
|
||||
export const NebulaConnectWallet = (props: {
|
||||
connectButtonClassName?: string;
|
||||
signInLinkButtonClassName?: string;
|
||||
detailsButtonClassName?: string;
|
||||
}) => {
|
||||
const thirdwebClient = useThirdwebClient();
|
||||
const router = useDashboardRouter();
|
||||
const { theme } = useTheme();
|
||||
const t = theme === "light" ? "light" : "dark";
|
||||
@@ -67,7 +66,7 @@ export const NebulaConnectWallet = (props: {
|
||||
return (
|
||||
<ConnectButton
|
||||
theme={getSDKTheme(t)}
|
||||
client={thirdwebClient}
|
||||
client={nebulaAppThirdwebClient}
|
||||
connectModal={{
|
||||
privacyPolicyUrl: "/privacy-policy",
|
||||
termsOfServiceUrl: "/terms",
|
||||
|
||||
@@ -1,3 +1,45 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import {
|
||||
IPFS_GATEWAY_URL,
|
||||
NEBULA_APP_SECRET_KEY,
|
||||
NEXT_PUBLIC_NEBULA_APP_CLIENT_ID,
|
||||
} from "@/constants/env";
|
||||
import {
|
||||
THIRDWEB_BUNDLER_DOMAIN,
|
||||
THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
THIRDWEB_PAY_DOMAIN,
|
||||
THIRDWEB_RPC_DOMAIN,
|
||||
THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
THIRDWEB_STORAGE_DOMAIN,
|
||||
} from "constants/urls";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { setThirdwebDomains } from "thirdweb/utils";
|
||||
import { getVercelEnv } from "../../../../lib/vercel-utils";
|
||||
|
||||
export const nebulaThirdwebClient = getThirdwebClient(undefined);
|
||||
// returns a thirdweb client with optional JWT passed in
|
||||
function getThirdwebClient() {
|
||||
if (getVercelEnv() !== "production") {
|
||||
// if not on production: run this when creating a client to set the domains
|
||||
setThirdwebDomains({
|
||||
rpc: THIRDWEB_RPC_DOMAIN,
|
||||
inAppWallet: THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
pay: THIRDWEB_PAY_DOMAIN,
|
||||
storage: THIRDWEB_STORAGE_DOMAIN,
|
||||
social: THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
bundler: THIRDWEB_BUNDLER_DOMAIN,
|
||||
insight: THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
});
|
||||
}
|
||||
|
||||
return createThirdwebClient({
|
||||
secretKey: NEBULA_APP_SECRET_KEY,
|
||||
clientId: NEXT_PUBLIC_NEBULA_APP_CLIENT_ID,
|
||||
config: {
|
||||
storage: {
|
||||
gatewayUrl: IPFS_GATEWAY_URL,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const nebulaAppThirdwebClient = getThirdwebClient();
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use server";
|
||||
import "server-only";
|
||||
|
||||
import {
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
NEXT_PUBLIC_NEBULA_URL,
|
||||
} from "@/constants/env";
|
||||
import { NEBULA_APP_SECRET_KEY, NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
|
||||
import { isVercel } from "lib/vercel-utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAddress } from "thirdweb";
|
||||
@@ -26,7 +23,7 @@ export async function getNebulaLoginPayload(
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Secret-Key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
"X-Secret-Key": NEBULA_APP_SECRET_KEY,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
address: params.address,
|
||||
@@ -70,7 +67,7 @@ export async function doNebulaLogin(
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Secret-Key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
"X-Secret-Key": NEBULA_APP_SECRET_KEY,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
+19
-19
@@ -1,6 +1,6 @@
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { projectStub } from "../../../stories/stubs";
|
||||
import { mobileViewport } from "../../../stories/utils";
|
||||
import { InAppWalletSettingsUI } from "./index";
|
||||
|
||||
const meta = {
|
||||
@@ -16,44 +16,44 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const GrowthPlan: Story = {
|
||||
args: {
|
||||
canEditAdvancedFeatures: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const FreePlan: Story = {
|
||||
args: {
|
||||
canEditAdvancedFeatures: false,
|
||||
currentPlan: "free",
|
||||
},
|
||||
};
|
||||
|
||||
export const GrowthPlanMobile: Story = {
|
||||
export const GrowthPlan: Story = {
|
||||
args: {
|
||||
canEditAdvancedFeatures: true,
|
||||
},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
currentPlan: "growth",
|
||||
},
|
||||
};
|
||||
|
||||
export const FreePlanMobile: Story = {
|
||||
export const AcceleratePlan: Story = {
|
||||
args: {
|
||||
canEditAdvancedFeatures: false,
|
||||
currentPlan: "accelerate",
|
||||
},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
};
|
||||
|
||||
export const GrowthLegacyPlan: Story = {
|
||||
args: {
|
||||
currentPlan: "growth_legacy",
|
||||
},
|
||||
};
|
||||
|
||||
export const ProPlan: Story = {
|
||||
args: {
|
||||
currentPlan: "pro",
|
||||
},
|
||||
};
|
||||
|
||||
function Variants(props: {
|
||||
canEditAdvancedFeatures: boolean;
|
||||
currentPlan: Team["billingPlan"];
|
||||
}) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1140px] px-4 py-6">
|
||||
<div className="flex flex-col gap-10">
|
||||
<InAppWalletSettingsUI
|
||||
canEditAdvancedFeatures={props.canEditAdvancedFeatures}
|
||||
teamPlan={props.currentPlan}
|
||||
project={projectStub("foo", "bar")}
|
||||
teamId="bar"
|
||||
embeddedWalletService={{
|
||||
|
||||
@@ -41,6 +41,7 @@ import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { upload } from "thirdweb/storage";
|
||||
import { toArrFromList } from "utils/string";
|
||||
import { planToTierRecordForGating } from "../../settings/Account/Billing/planToTierRecord";
|
||||
import { FileInput } from "../../shared/FileInput";
|
||||
import CountrySelector from "./sms-country-select/country-selector";
|
||||
|
||||
@@ -49,7 +50,7 @@ type InAppWalletSettingsPageProps = {
|
||||
project: Project;
|
||||
teamId: string;
|
||||
teamSlug: string;
|
||||
validTeamPlan: Team["billingPlan"];
|
||||
teamPlan: Team["billingPlan"];
|
||||
smsCountryTiers: SMSCountryTiers;
|
||||
};
|
||||
|
||||
@@ -113,7 +114,6 @@ export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
|
||||
return (
|
||||
<InAppWalletSettingsPageUI
|
||||
{...props}
|
||||
canEditAdvancedFeatures={props.validTeamPlan !== "free"}
|
||||
updateApiKey={handleUpdateProject}
|
||||
isUpdating={updateProject.isPending}
|
||||
smsCountryTiers={props.smsCountryTiers}
|
||||
@@ -123,7 +123,6 @@ export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
|
||||
|
||||
const InAppWalletSettingsPageUI: React.FC<
|
||||
InAppWalletSettingsPageProps & {
|
||||
canEditAdvancedFeatures: boolean;
|
||||
updateApiKey: (
|
||||
projectValues: Partial<Project>,
|
||||
trackingData: UpdateAPIKeyTrackingData,
|
||||
@@ -163,8 +162,7 @@ const InAppWalletSettingsPageUI: React.FC<
|
||||
};
|
||||
|
||||
export const InAppWalletSettingsUI: React.FC<
|
||||
Omit<InAppWalletSettingsPageProps, "validTeamPlan"> & {
|
||||
canEditAdvancedFeatures: boolean;
|
||||
InAppWalletSettingsPageProps & {
|
||||
updateApiKey: (
|
||||
projectValues: Partial<Project>,
|
||||
trackingData: UpdateAPIKeyTrackingData,
|
||||
@@ -173,7 +171,6 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
embeddedWalletService: ProjectEmbeddedWalletsService;
|
||||
}
|
||||
> = (props) => {
|
||||
const { canEditAdvancedFeatures } = props;
|
||||
const services = props.project.services;
|
||||
|
||||
const config = props.embeddedWalletService;
|
||||
@@ -181,6 +178,13 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
const hasCustomBranding =
|
||||
!!config.applicationImageUrl?.length || !!config.applicationName?.length;
|
||||
|
||||
const authRequiredPlan = "accelerate";
|
||||
|
||||
// accelerate or higher plan required
|
||||
const canEditSmsCountries =
|
||||
planToTierRecordForGating[props.teamPlan] >=
|
||||
planToTierRecordForGating[authRequiredPlan];
|
||||
|
||||
const form = useForm<ApiKeyEmbeddedWalletsValidationSchema>({
|
||||
resolver: zodResolver(apiKeyEmbeddedWalletsValidationSchema),
|
||||
values: {
|
||||
@@ -197,7 +201,7 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
redirectUrls: (config.redirectUrls || []).join("\n"),
|
||||
smsEnabledCountryISOs: config.smsEnabledCountryISOs
|
||||
? config.smsEnabledCountryISOs
|
||||
: canEditAdvancedFeatures
|
||||
: canEditSmsCountries
|
||||
? ["US", "CA"]
|
||||
: [],
|
||||
},
|
||||
@@ -269,7 +273,9 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
{/* Branding */}
|
||||
<BrandingFieldset
|
||||
form={form}
|
||||
canEditAdvancedFeatures={canEditAdvancedFeatures}
|
||||
teamPlan={props.teamPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
requiredPlan={authRequiredPlan}
|
||||
/>
|
||||
|
||||
<NativeAppsFieldset form={form} />
|
||||
@@ -278,22 +284,28 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
<Fieldset legend="Authentication">
|
||||
<JSONWebTokenFields
|
||||
form={form}
|
||||
canEditAdvancedFeatures={canEditAdvancedFeatures}
|
||||
teamPlan={props.teamPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
requiredPlan={authRequiredPlan}
|
||||
/>
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
<AuthEndpointFields
|
||||
form={form}
|
||||
canEditAdvancedFeatures={canEditAdvancedFeatures}
|
||||
teamPlan={props.teamPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
requiredPlan={authRequiredPlan}
|
||||
/>
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
<SMSCountryFields
|
||||
form={form}
|
||||
canEditAdvancedFeatures={canEditAdvancedFeatures}
|
||||
smsCountryTiers={props.smsCountryTiers}
|
||||
teamPlan={props.teamPlan}
|
||||
requiredPlan={authRequiredPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
@@ -310,10 +322,10 @@ export const InAppWalletSettingsUI: React.FC<
|
||||
|
||||
function BrandingFieldset(props: {
|
||||
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
|
||||
canEditAdvancedFeatures: boolean;
|
||||
teamPlan: Team["billingPlan"];
|
||||
teamSlug: string;
|
||||
requiredPlan: Team["billingPlan"];
|
||||
}) {
|
||||
const { form, canEditAdvancedFeatures } = props;
|
||||
|
||||
return (
|
||||
<Fieldset legend="Branding">
|
||||
<SwitchContainer
|
||||
@@ -322,31 +334,36 @@ function BrandingFieldset(props: {
|
||||
description="Pass a custom logo and app name to be used in the emails sent to users."
|
||||
>
|
||||
<GatedSwitch
|
||||
id="branding-switch"
|
||||
requiredPlan={props.requiredPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
trackingLabel="customEmailLogoAndName"
|
||||
checked={!!form.watch("branding") && canEditAdvancedFeatures}
|
||||
upgradeRequired={!canEditAdvancedFeatures}
|
||||
onCheckedChange={(checked) =>
|
||||
form.setValue(
|
||||
"branding",
|
||||
checked
|
||||
? {
|
||||
applicationImageUrl: "",
|
||||
applicationName: "",
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
currentPlan={props.teamPlan}
|
||||
switchProps={{
|
||||
id: "branding-switch",
|
||||
checked: !!props.form.watch("branding"),
|
||||
onCheckedChange: (checked) =>
|
||||
props.form.setValue(
|
||||
"branding",
|
||||
checked
|
||||
? {
|
||||
applicationImageUrl: "",
|
||||
applicationName: "",
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</SwitchContainer>
|
||||
|
||||
<AdvancedConfigurationContainer
|
||||
<GatedCollapsibleContainer
|
||||
requiredPlan={props.requiredPlan}
|
||||
currentPlan={props.teamPlan}
|
||||
className="grid grid-cols-1 gap-6 lg:grid-cols-2"
|
||||
show={canEditAdvancedFeatures && !!form.watch("branding")}
|
||||
isExpanded={!!props.form.watch("branding")}
|
||||
>
|
||||
{/* Application Image */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
control={props.form.control}
|
||||
name="branding.applicationImageUrl"
|
||||
render={() => (
|
||||
<FormItem className="space-y-1">
|
||||
@@ -358,9 +375,9 @@ function BrandingFieldset(props: {
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<AppImageFormControl
|
||||
uri={form.watch("branding.applicationImageUrl")}
|
||||
uri={props.form.watch("branding.applicationImageUrl")}
|
||||
setUri={(uri) => {
|
||||
form.setValue("branding.applicationImageUrl", uri, {
|
||||
props.form.setValue("branding.applicationImageUrl", uri, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
@@ -374,7 +391,7 @@ function BrandingFieldset(props: {
|
||||
|
||||
{/* Application Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
control={props.form.control}
|
||||
name="branding.applicationName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
@@ -391,7 +408,7 @@ function BrandingFieldset(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</AdvancedConfigurationContainer>
|
||||
</GatedCollapsibleContainer>
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
@@ -453,8 +470,10 @@ function AppImageFormControl(props: {
|
||||
|
||||
function SMSCountryFields(props: {
|
||||
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
|
||||
canEditAdvancedFeatures: boolean;
|
||||
smsCountryTiers: SMSCountryTiers;
|
||||
teamPlan: Team["billingPlan"];
|
||||
requiredPlan: Team["billingPlan"];
|
||||
teamSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
@@ -464,31 +483,30 @@ function SMSCountryFields(props: {
|
||||
description="Optionally allow users in selected countries to login via SMS OTP."
|
||||
>
|
||||
<GatedSwitch
|
||||
id="sms-switch"
|
||||
currentPlan={props.teamPlan}
|
||||
requiredPlan={props.requiredPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
switchProps={{
|
||||
id: "sms-switch",
|
||||
checked: !!props.form.watch("smsEnabledCountryISOs").length,
|
||||
onCheckedChange: (checked) =>
|
||||
props.form.setValue(
|
||||
"smsEnabledCountryISOs",
|
||||
checked
|
||||
? // by default, enable US and CA only
|
||||
["US", "CA"]
|
||||
: [],
|
||||
),
|
||||
}}
|
||||
trackingLabel="sms"
|
||||
checked={
|
||||
!!props.form.watch("smsEnabledCountryISOs").length &&
|
||||
props.canEditAdvancedFeatures
|
||||
}
|
||||
upgradeRequired={!props.canEditAdvancedFeatures}
|
||||
onCheckedChange={(checked) =>
|
||||
props.form.setValue(
|
||||
"smsEnabledCountryISOs",
|
||||
checked
|
||||
? // by default, enable US and CA only
|
||||
["US", "CA"]
|
||||
: [],
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SwitchContainer>
|
||||
|
||||
<AdvancedConfigurationContainer
|
||||
<GatedCollapsibleContainer
|
||||
className="grid grid-cols-1"
|
||||
show={
|
||||
props.canEditAdvancedFeatures &&
|
||||
!!props.form.watch("smsEnabledCountryISOs").length
|
||||
}
|
||||
currentPlan={props.teamPlan}
|
||||
requiredPlan={props.requiredPlan}
|
||||
isExpanded={!!props.form.watch("smsEnabledCountryISOs").length}
|
||||
>
|
||||
<FormField
|
||||
control={props.form.control}
|
||||
@@ -501,17 +519,17 @@ function SMSCountryFields(props: {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</AdvancedConfigurationContainer>
|
||||
</GatedCollapsibleContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function JSONWebTokenFields(props: {
|
||||
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
|
||||
canEditAdvancedFeatures: boolean;
|
||||
teamPlan: Team["billingPlan"];
|
||||
teamSlug: string;
|
||||
requiredPlan: Team["billingPlan"];
|
||||
}) {
|
||||
const { form, canEditAdvancedFeatures } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SwitchContainer
|
||||
@@ -533,32 +551,31 @@ function JSONWebTokenFields(props: {
|
||||
}
|
||||
>
|
||||
<GatedSwitch
|
||||
id="authentication-switch"
|
||||
upgradeRequired={!canEditAdvancedFeatures}
|
||||
trackingLabel="customAuthJWT"
|
||||
checked={
|
||||
!!form.watch("customAuthentication") && canEditAdvancedFeatures
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
form.setValue(
|
||||
"customAuthentication",
|
||||
checked
|
||||
? {
|
||||
jwksUri: "",
|
||||
aud: "",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
currentPlan={props.teamPlan}
|
||||
requiredPlan={props.requiredPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
switchProps={{
|
||||
id: "authentication-switch",
|
||||
checked: !!props.form.watch("customAuthentication"),
|
||||
onCheckedChange: (checked) => {
|
||||
props.form.setValue(
|
||||
"customAuthentication",
|
||||
checked ? { jwksUri: "", aud: "" } : undefined,
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SwitchContainer>
|
||||
|
||||
<AdvancedConfigurationContainer
|
||||
<GatedCollapsibleContainer
|
||||
className="grid grid-cols-1 gap-6 lg:grid-cols-2"
|
||||
show={canEditAdvancedFeatures && !!form.watch("customAuthentication")}
|
||||
isExpanded={!!props.form.watch("customAuthentication")}
|
||||
currentPlan={props.teamPlan}
|
||||
requiredPlan={props.requiredPlan}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
control={props.form.control}
|
||||
name="customAuthentication.jwksUri"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
@@ -576,7 +593,7 @@ function JSONWebTokenFields(props: {
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
control={props.form.control}
|
||||
name="customAuthentication.aud"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
@@ -591,19 +608,19 @@ function JSONWebTokenFields(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</AdvancedConfigurationContainer>
|
||||
</GatedCollapsibleContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthEndpointFields(props: {
|
||||
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
|
||||
canEditAdvancedFeatures: boolean;
|
||||
teamPlan: Team["billingPlan"];
|
||||
teamSlug: string;
|
||||
requiredPlan: Team["billingPlan"];
|
||||
}) {
|
||||
const { form, canEditAdvancedFeatures } = props;
|
||||
|
||||
const expandCustomAuthEndpointField =
|
||||
form.watch("customAuthEndpoint") !== undefined && canEditAdvancedFeatures;
|
||||
props.form.watch("customAuthEndpoint") !== undefined;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -628,28 +645,31 @@ function AuthEndpointFields(props: {
|
||||
>
|
||||
<GatedSwitch
|
||||
trackingLabel="customAuthEndpoint"
|
||||
checked={expandCustomAuthEndpointField}
|
||||
upgradeRequired={!canEditAdvancedFeatures}
|
||||
onCheckedChange={(checked) => {
|
||||
form.setValue(
|
||||
"customAuthEndpoint",
|
||||
checked
|
||||
? {
|
||||
authEndpoint: "",
|
||||
customHeaders: [],
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
switchProps={{
|
||||
id: "auth-endpoint-switch",
|
||||
checked: expandCustomAuthEndpointField,
|
||||
onCheckedChange: (checked) => {
|
||||
props.form.setValue(
|
||||
"customAuthEndpoint",
|
||||
checked
|
||||
? {
|
||||
authEndpoint: "",
|
||||
customHeaders: [],
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
}}
|
||||
currentPlan={props.teamPlan}
|
||||
requiredPlan={props.requiredPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
</SwitchContainer>
|
||||
|
||||
{/* useFieldArray used on this component - it creates empty customAuthEndpoint.customHeaders array on mount */}
|
||||
{/* So only mount if expandCustomAuthEndpointField is true */}
|
||||
{expandCustomAuthEndpointField && (
|
||||
<AuthEndpointFieldsContent
|
||||
form={form}
|
||||
canEditAdvancedFeatures={canEditAdvancedFeatures}
|
||||
/>
|
||||
<AuthEndpointFieldsContent form={props.form} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -657,19 +677,16 @@ function AuthEndpointFields(props: {
|
||||
|
||||
function AuthEndpointFieldsContent(props: {
|
||||
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
|
||||
canEditAdvancedFeatures: boolean;
|
||||
}) {
|
||||
const { form } = props;
|
||||
|
||||
const customHeaderFields = useFieldArray({
|
||||
control: form.control,
|
||||
control: props.form.control,
|
||||
name: "customAuthEndpoint.customHeaders",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
control={props.form.control}
|
||||
name="customAuthEndpoint.authEndpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
@@ -698,14 +715,14 @@ function AuthEndpointFieldsContent(props: {
|
||||
<Input
|
||||
placeholder="Name"
|
||||
type="text"
|
||||
{...form.register(
|
||||
{...props.form.register(
|
||||
`customAuthEndpoint.customHeaders.${customHeaderIdx}.key`,
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Value"
|
||||
type="text"
|
||||
{...form.register(
|
||||
{...props.form.register(
|
||||
`customAuthEndpoint.customHeaders.${customHeaderIdx}.value`,
|
||||
)}
|
||||
/>
|
||||
@@ -781,12 +798,18 @@ function NativeAppsFieldset(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedConfigurationContainer(props: {
|
||||
function GatedCollapsibleContainer(props: {
|
||||
children: React.ReactNode;
|
||||
show: boolean;
|
||||
isExpanded: boolean;
|
||||
className?: string;
|
||||
requiredPlan: Team["billingPlan"];
|
||||
currentPlan: Team["billingPlan"];
|
||||
}) {
|
||||
if (!props.show) {
|
||||
const upgradeRequired =
|
||||
planToTierRecordForGating[props.currentPlan] <
|
||||
planToTierRecordForGating[props.requiredPlan];
|
||||
|
||||
if (!props.isExpanded || upgradeRequired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -800,7 +823,7 @@ function Fieldset(props: {
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<fieldset className="rounded-lg border border-border bg-card p-4 md:p-6">
|
||||
{/* put inside div to remove defualt styles on legend */}
|
||||
{/* put inside div to remove default styles on legend */}
|
||||
<div className="mb-4 font-semibold text-xl tracking-tight">
|
||||
<legend> {props.legend}</legend>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { Team } from "@/api/team";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { GatedSwitch } from "./GatedSwitch";
|
||||
|
||||
@@ -18,41 +27,52 @@ export const AllVariants: Story = {
|
||||
};
|
||||
|
||||
function Variants() {
|
||||
const [requiredPlan, setRequiredPlan] = useState<
|
||||
"free" | "growth" | "accelerate" | "pro"
|
||||
>("accelerate");
|
||||
|
||||
const plans: Team["billingPlan"][] = [
|
||||
"free",
|
||||
"starter_legacy",
|
||||
"starter",
|
||||
"growth_legacy",
|
||||
"growth",
|
||||
"accelerate",
|
||||
"pro",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-background p-7 text-foreground">
|
||||
<div className="mx-auto flex max-w-[500px] flex-col gap-8">
|
||||
<BadgeContainer label="upgradeRequired">
|
||||
<GatedSwitch upgradeRequired />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No upgradeRequired">
|
||||
<GatedSwitch upgradeRequired={false} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No upgradeRequired, disabled">
|
||||
<GatedSwitch upgradeRequired={false} disabled />
|
||||
</BadgeContainer>
|
||||
|
||||
<Separator />
|
||||
|
||||
<BadgeContainer label="upgradeRequired, Checked">
|
||||
<GatedSwitch upgradeRequired checked />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="upgradeRequired, unchecked">
|
||||
<GatedSwitch upgradeRequired checked={false} />
|
||||
</BadgeContainer>
|
||||
|
||||
<Separator />
|
||||
|
||||
<BadgeContainer label="No upgradeRequired, Checked">
|
||||
<GatedSwitch upgradeRequired={false} checked />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No upgradeRequired, unchecked">
|
||||
<GatedSwitch upgradeRequired={false} checked={false} />
|
||||
</BadgeContainer>
|
||||
<div className="container flex max-w-5xl flex-col gap-10 py-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Required Plan</Label>
|
||||
<Select
|
||||
value={requiredPlan}
|
||||
onValueChange={(value) =>
|
||||
setRequiredPlan(value as typeof requiredPlan)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select plan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="free">Free</SelectItem>
|
||||
<SelectItem value="growth">Growth</SelectItem>
|
||||
<SelectItem value="accelerate">Accelerate</SelectItem>
|
||||
<SelectItem value="pro">Pro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{plans.map((currentPlan) => (
|
||||
<BadgeContainer key={currentPlan} label={`plan: ${currentPlan}`}>
|
||||
<GatedSwitch
|
||||
key={currentPlan}
|
||||
currentPlan={currentPlan}
|
||||
requiredPlan={requiredPlan}
|
||||
teamSlug="foo"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { Team } from "@/api/team";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TeamPlanBadge } from "../../../../app/(app)/components/TeamPlanBadge";
|
||||
import { planToTierRecordForGating } from "./planToTierRecord";
|
||||
|
||||
type SwitchProps = React.ComponentProps<typeof Switch>;
|
||||
|
||||
interface GatedSwitchProps extends SwitchProps {
|
||||
type GatedSwitchProps = {
|
||||
trackingLabel?: string;
|
||||
upgradeRequired: boolean;
|
||||
}
|
||||
currentPlan: Team["billingPlan"];
|
||||
requiredPlan: Team["billingPlan"];
|
||||
teamSlug: string;
|
||||
switchProps?: SwitchProps;
|
||||
};
|
||||
|
||||
export const GatedSwitch: React.FC<GatedSwitchProps> = (
|
||||
allProps: GatedSwitchProps,
|
||||
props: GatedSwitchProps,
|
||||
) => {
|
||||
const { upgradeRequired, trackingLabel, checked, ...props } = allProps;
|
||||
const isUpgradeRequired =
|
||||
planToTierRecordForGating[props.currentPlan] <
|
||||
planToTierRecordForGating[props.requiredPlan];
|
||||
|
||||
return (
|
||||
<ToolTipLabel
|
||||
hoverable
|
||||
contentClassName="max-w-[300px]"
|
||||
label={
|
||||
upgradeRequired ? (
|
||||
<div className="w-[220px]">
|
||||
To access this feature, you need to upgrade to the{" "}
|
||||
isUpgradeRequired ? (
|
||||
<span>
|
||||
To access this feature, <br /> Upgrade to the{" "}
|
||||
<TrackedLinkTW
|
||||
target="_blank"
|
||||
href="/team/~/~/settings/billing"
|
||||
href={`/team/${props.teamSlug}/~/settings/billing`}
|
||||
category="advancedFeature"
|
||||
label={trackingLabel}
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
label={props.trackingLabel}
|
||||
className="text-link-foreground capitalize hover:text-foreground"
|
||||
>
|
||||
Growth plan
|
||||
{props.requiredPlan} plan
|
||||
</TrackedLinkTW>
|
||||
.
|
||||
</div>
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{upgradeRequired && <Badge>Growth</Badge>}
|
||||
{isUpgradeRequired && <TeamPlanBadge plan={props.requiredPlan} />}
|
||||
<Switch
|
||||
checked={checked}
|
||||
disabled={upgradeRequired || props.disabled}
|
||||
{...props}
|
||||
{...props.switchProps}
|
||||
checked={props.switchProps?.checked && !isUpgradeRequired}
|
||||
disabled={props.switchProps?.disabled || isUpgradeRequired}
|
||||
className={cn("disabled:opacity-100", props.switchProps?.className)}
|
||||
/>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Team } from "@/api/team";
|
||||
|
||||
// Note: Growth legacy is considered higher tier in this hierarchy
|
||||
export const planToTierRecordForGating: Record<Team["billingPlan"], number> = {
|
||||
free: 0,
|
||||
starter_legacy: 1,
|
||||
starter: 2,
|
||||
growth: 3,
|
||||
accelerate: 4,
|
||||
growth_legacy: 5,
|
||||
scale: 6,
|
||||
pro: 7,
|
||||
};
|
||||
@@ -32,7 +32,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import type { ProjectService } from "@thirdweb-dev/service-utils";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -529,7 +529,7 @@ function CreatedProjectDetails(props: {
|
||||
className="min-w-28 gap-2"
|
||||
>
|
||||
View Project
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
@@ -41,6 +41,7 @@ type AccountAbstractionSettingsPageProps = {
|
||||
project: Project;
|
||||
trackingCategory: string;
|
||||
teamId: string;
|
||||
teamSlug: string;
|
||||
validTeamPlan: Team["billingPlan"];
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
@@ -548,7 +549,12 @@ export function AccountAbstractionSettingsPage(
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<div className="flex flex-row items-center justify-between gap-6 lg:gap-12">
|
||||
<div>
|
||||
<FormLabel pointerEvents="none">Server verifier</FormLabel>
|
||||
<FormLabel
|
||||
pointerEvents="none"
|
||||
htmlFor="server-verifier-switch"
|
||||
>
|
||||
Server verifier
|
||||
</FormLabel>
|
||||
<Text>
|
||||
Specify your own endpoint that will verify each transaction
|
||||
and decide whether it should be sponsored or not. <br /> This
|
||||
@@ -566,18 +572,20 @@ export function AccountAbstractionSettingsPage(
|
||||
</div>
|
||||
|
||||
<GatedSwitch
|
||||
upgradeRequired={props.validTeamPlan === "free"}
|
||||
checked={
|
||||
form.watch("serverVerifier").enabled &&
|
||||
props.validTeamPlan !== "free"
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
form.setValue(
|
||||
"serverVerifier",
|
||||
!checked
|
||||
? { enabled: false, url: null, headers: null }
|
||||
: { enabled: true, url: "", headers: [] },
|
||||
);
|
||||
requiredPlan="accelerate"
|
||||
currentPlan={props.validTeamPlan}
|
||||
teamSlug={props.teamSlug}
|
||||
switchProps={{
|
||||
id: "server-verifier-switch",
|
||||
checked: form.watch("serverVerifier").enabled,
|
||||
onCheckedChange: (checked) => {
|
||||
form.setValue(
|
||||
"serverVerifier",
|
||||
!checked
|
||||
? { enabled: false, url: null, headers: null }
|
||||
: { enabled: true, url: "", headers: [] },
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -79,6 +79,7 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
|
||||
insight: {
|
||||
enabled: true,
|
||||
rateLimit: 1000,
|
||||
webhooks: true,
|
||||
},
|
||||
embeddedWallets: {
|
||||
enabled: true,
|
||||
@@ -96,6 +97,7 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
|
||||
},
|
||||
},
|
||||
planCancellationDate: null,
|
||||
unthreadCustomerId: null,
|
||||
};
|
||||
|
||||
return team;
|
||||
|
||||
@@ -867,6 +867,50 @@ const infrastructureRedirects = {
|
||||
"/rpc-edge/faqs": "/infrastructure/rpc-edge/overview",
|
||||
};
|
||||
|
||||
const glossaryRedirects = {
|
||||
"/glossary": "https://thirdweb.com/learn/glossary",
|
||||
"/claim-phases": "https://thirdweb.com/learn/glossary/claim-phases",
|
||||
"/glossary/claiming": "https://thirdweb.com/learn/glossary/claim-phases",
|
||||
"/glossary/composability":
|
||||
"https://thirdweb.com/learn/glossary/composability",
|
||||
"/glossary/delayed-reveal":
|
||||
"https://thirdweb.com/learn/glossary/delayed-reveal",
|
||||
"/glossary/drop": "https://thirdweb.com/learn/glossary/drop",
|
||||
"/glossary/externally-owned-account":
|
||||
"https://thirdweb.com/learn/glossary/externally-owned-account",
|
||||
"/glossary/factory-contract":
|
||||
"https://thirdweb.com/learn/glossary/factory-contract",
|
||||
"/glossary/gas": "https://thirdweb.com/learn/glossary/gas-fee",
|
||||
"/glossary/gasless-transactions":
|
||||
"https://thirdweb.com/learn/glossary/gasless-transactions",
|
||||
"/glossary/interoperability":
|
||||
"https://thirdweb.com/learn/glossary/interoperability",
|
||||
"/glossary/ipfs":
|
||||
"https://thirdweb.com/learn/glossary/interplanetary-file-system-ipfs",
|
||||
"/glossary/lazy-minting": "https://thirdweb.com/learn/glossary/lazy-minting",
|
||||
"/glossary/local-wallet": "https://thirdweb.com/learn/glossary/local-wallet",
|
||||
"/glossary/minting": "https://thirdweb.com/learn/glossary/minting",
|
||||
"/glossary/nft": "https://thirdweb.com/learn/glossary/nft",
|
||||
"/glossary/non-custodial-wallet":
|
||||
"https://thirdweb.com/learn/glossary/non-custodial-wallet",
|
||||
"/glossary/permissionless":
|
||||
"https://thirdweb.com/learn/glossary/permissionless",
|
||||
"/glossary/proxy-contracts":
|
||||
"https://thirdweb.com/learn/glossary/proxy-contracts",
|
||||
"/glossary/rpc": "https://thirdweb.com/learn/glossary/rpc-url",
|
||||
"/glossary/signature-based-minting":
|
||||
"https://thirdweb.com/learn/glossary/signature-based-minting",
|
||||
"/glossary/smart-contract":
|
||||
"https://thirdweb.com/learn/glossary/smart-contract",
|
||||
"/glossary/smart-account":
|
||||
"https://thirdweb.com/learn/glossary/smart-account",
|
||||
"/glossary/soulbound": "https://thirdweb.com/learn/glossary/soulbound-tokens",
|
||||
"/glossary/staking": "https://thirdweb.com/learn/glossary/staking",
|
||||
"/glossary/token": "https://thirdweb.com/learn/glossary/token",
|
||||
"/glossary/wallet": "https://thirdweb.com/learn/glossary/",
|
||||
"/glossary/bundler": "https://thirdweb.com/learn/glossary/bundler",
|
||||
};
|
||||
|
||||
const otherRedirects = {
|
||||
// wallets > connect redirects
|
||||
"/wallets": "/connect",
|
||||
@@ -1020,6 +1064,7 @@ export const redirects = async () => {
|
||||
...createRedirects(v5RestructuredRedirects),
|
||||
...createRedirects(unrealEngineRedirects),
|
||||
...createRedirects(v4ToV5Redirects),
|
||||
...createRedirects(glossaryRedirects),
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 309 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 797 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 694 KiB |
@@ -0,0 +1,36 @@
|
||||
import { Callout, Step, Steps, DocImage } from "@doc";
|
||||
import ProjOverview from "./assets/proj-overview.png";
|
||||
import ProjSettings from "./assets/proj-settings.png";
|
||||
import ProjTransfer from "./assets/proj-transfer.png";
|
||||
|
||||
|
||||
# Transfer projects to another team
|
||||
|
||||
Learn how to transfer a thirdweb project from one team to another.
|
||||
|
||||
<Callout variant="info" title="Owner Permissions Required">
|
||||
To transfer projects, you must be an owner of both the source and destination teams.
|
||||
</Callout>
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Select Project">
|
||||
Select the project you want to transfer from the list of projects in your team.
|
||||
|
||||
<DocImage src={ProjOverview} alt="Project overview"/>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Project Settings">
|
||||
Navigate to the project settings by clicking on the gear icon in the bottom left of the project dashboard.
|
||||
<DocImage src={ProjSettings} alt="Project settings"/>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Transfer Project">
|
||||
Navigate to the Transfer Project modal and select the destination team from the dropdown. Click on the Transfer Project button to initiate the transfer.
|
||||
|
||||
<DocImage src={ProjTransfer} alt="Transfer project"/>
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
@@ -38,7 +38,7 @@ export const sidebar: SideBar = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "API Keys",
|
||||
name: "Projects",
|
||||
links: [
|
||||
{
|
||||
name: "Overview",
|
||||
@@ -64,6 +64,10 @@ export const sidebar: SideBar = {
|
||||
name: "Access Restrictions",
|
||||
href: `${accountSlug}/api-keys/access`,
|
||||
},
|
||||
{
|
||||
name: "Transfer Projects",
|
||||
href: `${accountSlug}/api-keys/transfer-projects`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 569 KiB |
@@ -0,0 +1,56 @@
|
||||
import {DocImage} from "@doc";
|
||||
import FeeShare from './assets/fee-dash.png';
|
||||
|
||||
|
||||
# Understanding fees
|
||||
|
||||
Understand how protocol, developer, and onramp fees work with Universal Bridge.
|
||||
|
||||
### Developer Fee
|
||||
|
||||
To monetize applications or games, you are able to set a developer fee on each crypto-to-crypto or swap transaction. You may set this percentage
|
||||
through Project > Universal Bridge > Settings.
|
||||
|
||||
<DocImage src={FeeShare} alt="Developer fee share" />
|
||||
|
||||
### Protocol Fee
|
||||
|
||||
Our protocol fee is a flat 30 basis points (0.30%) applied on the source token amount.
|
||||
This fee is independent of network gas fees and does not fluctuate.
|
||||
|
||||
#### Setting fees using buy or sell
|
||||
|
||||
You can configure who pays the protocol fee for buy or sell transactions by specifying the input amount `sell` or `buy`.
|
||||
|
||||
See full reference for [`buy`](https://portal.thirdweb.com/references/typescript/v5/buy/prepare) and [`sell`](https://portal.thirdweb.com/references/typescript/v5/sell/prepare).
|
||||
|
||||
|
||||
#### Setting fees on transfers or purchase mode
|
||||
|
||||
You can configure who pays the protocol fee for transfers or purchase mode using the `feePayer` setting. This affects the
|
||||
total amount paid by the user and the amount received by the destination (e.g., a seller, app, or partner).
|
||||
|
||||
feePayer=buyer (Default) i.e. the user covers the protocol fee.
|
||||
|
||||
Example: If the source token is 10 USDC, the user pays 10.30 USDC.
|
||||
|
||||
- thirdweb receives: 0.30 USDC
|
||||
|
||||
- Receiver receives: 10.00 USDC
|
||||
|
||||
feePayer=receiver i.e. the receiver covers the protocol fee.
|
||||
|
||||
Example: The user pays 10.00 USDC.
|
||||
|
||||
- thirdweb receives: 0.30 USDC
|
||||
|
||||
- Receiver receives: 9.70 USDC
|
||||
|
||||
[See full reference.](https://bridge.thirdweb.com/reference#tag/transfer/POST/v1/transfer/prepare)
|
||||
|
||||
### Onramp Fee
|
||||
|
||||
When onramping to crypto using the Universal Bridge, a fee is charged by the third-party provider. This fee is determined by the provider and is typically a percentage
|
||||
of the amount being onramped. This fee may vary based on the provider and payment method used.
|
||||
|
||||
There are no protocol fees on onramp transactions.
|
||||
@@ -232,6 +232,10 @@ export const sidebar: SideBar = {
|
||||
name: "Supported Routes",
|
||||
href: "https://thirdweb.com/routes",
|
||||
},
|
||||
{
|
||||
name: "Service Fees",
|
||||
href: `${paySlug}/fees`,
|
||||
},
|
||||
{
|
||||
name: "Get Started",
|
||||
href: `${paySlug}/get-started`,
|
||||
|
||||
@@ -644,10 +644,6 @@ export const sidebar: SideBar = {
|
||||
name: "ERC20ClaimConditions",
|
||||
href: `${extensionsSlug}/erc-20/ERC20ClaimConditions`,
|
||||
},
|
||||
{
|
||||
name: "ERC20ClaimPhases",
|
||||
href: `${extensionsSlug}/erc-20/ERC20ClaimPhases`,
|
||||
},
|
||||
{
|
||||
name: "ERC20Mintable",
|
||||
href: `${extensionsSlug}/erc-20/ERC20Mintable`,
|
||||
@@ -791,10 +787,6 @@ export const sidebar: SideBar = {
|
||||
{
|
||||
name: "ERC-4337",
|
||||
links: [
|
||||
{
|
||||
name: "AccountModule",
|
||||
href: `${extensionsSlug}/erc-4337/AccountModule`,
|
||||
},
|
||||
{
|
||||
name: "SmartWallet",
|
||||
href: `${extensionsSlug}/erc-4337/SmartWallet`,
|
||||
|
||||
@@ -26,6 +26,13 @@ Engine enables your app to:
|
||||
|
||||
🚀 [Get Engine hosted by thirdweb](https://thirdweb.com/team/~/~/engine/create) or [self-host for free](/engine/self-host).
|
||||
|
||||
### Tiers
|
||||
|
||||
Engine is available in two tiers: Cloud and Dedicated.
|
||||
- **Cloud**: thirdweb-hosted Engine with autoscaling and high availability.
|
||||
- **Dedicated**: Self-hosted Engine with full control over your infrastructure.
|
||||
|
||||
|
||||
<DocImage
|
||||
src={OverviewImage}
|
||||
alt="Engine overview"
|
||||
|
||||
@@ -9,6 +9,10 @@ export const sidebar: SideBar = {
|
||||
name: "Overview",
|
||||
href: "/engine",
|
||||
},
|
||||
{
|
||||
name: "Playground",
|
||||
href: "https://playground.thirdweb.com/engine/airdrop",
|
||||
},
|
||||
{
|
||||
name: "Get Started",
|
||||
href: `${engineSlug}/get-started`,
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Breadcrumb } from "@/components/Document/Breadcrumb";
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function GlossaryBreadcrumb(props: { sidebar: SideBar }) {
|
||||
const pathname = usePathname();
|
||||
const currentLink = props.sidebar.links.find(
|
||||
(link) => "href" in link && link.href === pathname,
|
||||
);
|
||||
|
||||
if (currentLink && "name" in currentLink) {
|
||||
return (
|
||||
<Breadcrumb
|
||||
crumbs={[
|
||||
{
|
||||
name: "Glossary",
|
||||
href: "/glossary",
|
||||
},
|
||||
{
|
||||
href: pathname,
|
||||
name: currentLink.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# Bundler
|
||||
|
||||
A bundler is a node that monitors the alternative mempool of `UserOperations` and bundles multiple `UserOps` together to forward to the EntryPoint contract as a single transaction.
|
||||
These `UserOps` can be sent from different accounts and are bundled and sent to the `EntryPoint` contract via a `handleOps` call.
|
||||
|
||||
The bundler is controlled by its own EOA which initially pays for the gas fees upfront and is then repaid by either the sender or a paymaster if the transaction is sponsored.
|
||||
|
||||
The entry point contract then uses the `validateOp` and `executeOp` functions on the smart account contract to verify and execute the `UserOps` on behalf of the users.
|
||||
|
||||
It allows you to send transactions with smart accounts.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Claim Phases
|
||||
|
||||
Claim phases or "claim conditions" allow you to configure various aspects of a [drop](/glossary/drop); such as who can claim tokens,
|
||||
how many tokens will be released, the price of each token, release date, etc.
|
||||
|
||||
They are available to use for any smart contract that implements the [Drop Single Phase](/contracts/build/extensions/general/DropSinglePhase)
|
||||
or [Drop](/contracts/build/extensions/general/Drop) extension smart contracts, which includes our ready-to-deploy smart contracts like the
|
||||
[NFT Drop](https://thirdweb.com/thirdweb.eth/DropERC721), [Token Drop](https://thirdweb.com/thirdweb.eth/DropERC20), and [Edition Drop](https://thirdweb.com/thirdweb.eth/DropERC1155) smart contracts.
|
||||
@@ -1,4 +0,0 @@
|
||||
# Claiming
|
||||
|
||||
Claiming is the process of transferring token ownership to the claimer's wallet.
|
||||
Once a token has been [lazy minted](/glossary/lazy-minting), it can be claimed by a wallet.
|
||||
@@ -1,10 +0,0 @@
|
||||
# Composability
|
||||
|
||||
Composability refers to the ability for different protocols and smart contracts to interact and build upon each other.
|
||||
It allows for the creation of new products and services without having to build everything from scratch.
|
||||
|
||||
Thirdweb is completely composable. Each product can be used independently or in combination with other products.
|
||||
For example, you can use the [SDKs](/connect) or [Dashboard](https://thirdweb.com/team) with **any** contract. Similarly, you can use the [Solidity SDK](/contracts/build/overview)
|
||||
without using the [Dashboard](https://thirdweb.com/team) or [TypeScript/JavaScript SDK](/typescript/v5).
|
||||
|
||||
So, if you already have your own workflow which works for you, you can pick and choose which products you want to use.
|
||||
@@ -1,14 +0,0 @@
|
||||
# Delayed Reveal
|
||||
|
||||
Delayed reveal is a feature that allows you to release NFTs in a [drop](/glossary/drop) with hidden content that can only be revealed at a later time.
|
||||
The owner of the smart contract (or a wallet with the appropriate permissions) can [lazy-mints](/glossary/lazy-minting) a batch of NFTs, along with placeholder metadata and a password.
|
||||
|
||||
Until the NFTs are revealed using this password, the metadata of the NFTs will be the placeholder metadata, and the "real" metadata is not visible to external parties.
|
||||
|
||||
This is achieved by using the password as an encryption/decryption key to encrypt the URI containing the "real" metadata.
|
||||
The encrypted URI is stored onchain, and the password is stored off-chain, only known by the owner.
|
||||
|
||||
When the NFTs are revealed, the owner of the smart contract (or a wallet with the appropriate permissions)
|
||||
can decrypt the URI using the password, and the "real" metadata is revealed.
|
||||
|
||||
[Learn more from our feature release product update](https://blog.thirdweb.com/delayed-reveal-nfts/).
|
||||
@@ -1,15 +0,0 @@
|
||||
# Drop
|
||||
|
||||
A drop smart contract allows the owner of the contract to [lazy mint](/glossary/lazy-minting) NFTs _(unless it is an ERC20 drop)_,
|
||||
and have other users mint them, typically under the conditions defined in the smart contract’s [claim phases](/glossary/claim-phases), although these are not required.
|
||||
|
||||
This is a common pattern for projects that want their community to be able to claim tokens:
|
||||
|
||||
1. The contract owner defines the metadata for the tokens:
|
||||
|
||||
- For NFTs, this means they [lazy mint](/glossary/lazy-minting) the NFTs by providing the metadata for each NFT.
|
||||
- For ERC20 tokens, the token metadata is defined when the contract is deployed.
|
||||
|
||||
2. The contract owner defines how user’s can claim the tokens, optionally configuring [claim phases](/glossary/claim-phases).
|
||||
|
||||
3. Users can claim tokens by calling the `claim` function on the contract, which will mint the tokens to the user’s address.
|
||||
@@ -1,4 +0,0 @@
|
||||
# Entry Point
|
||||
|
||||
The EntryPoint is a singleton contract (a contract that has a predictable address that is the same on every chain and only one instance per chain).
|
||||
It has two methods that are used as entry points to execute bundles of UserOperations: `handleOps` and `handleAggregatedOps` which validate and execute user operation(s).
|
||||
@@ -1,4 +0,0 @@
|
||||
# Externally Owned Account
|
||||
|
||||
An Externally Owned Account (EOA) is an account controlled by the owner of the account's private key usually through a wallet application.
|
||||
Examples include MetaMask, Coinbase Wallet, and Phantom.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Factory contract
|
||||
|
||||
A factory contract is a smart contract that is used to deploy other smart contracts.
|
||||
@@ -1,17 +0,0 @@
|
||||
# Gas Fees
|
||||
|
||||
All transactions (operations that change information stored on the blockchain) require a gas fee,
|
||||
which is used to cover the costs of the computational resources to execute that transaction.
|
||||
|
||||
Gas fees are typically paid in the currency that is native to the blockchain you are interacting with. For example, if you are using the Ethereum blockchain, you will need to pay gas fees in Ether.
|
||||
|
||||
While using thirdweb is free, you will need to cover the cost of performing actions that update the blockchain's state.
|
||||
|
||||
This includes actions performed via the thirdweb [dashboard](https://thirdweb.com/team), our [SDKs](/contracts), or any of our other products, including:
|
||||
|
||||
- Deploying smart contracts
|
||||
- [Lazy-minting](/glossary/lazy-minting) the metadata of your NFTs
|
||||
- Updating permissions on your smart contracts
|
||||
- Setting [claim phases](/glossary/claim-phases)
|
||||
|
||||
[Learn more on the official Ethereum documentation](https://ethereum.org/en/developers/docs/gas/).
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ArticleIconCard, Callout, Stack } from "@doc";
|
||||
import { GraduationCap } from "lucide-react";
|
||||
|
||||
# Gasless Transactions
|
||||
|
||||
Gasless Transactions, also known as meta-transactions, are transactions that have the [gas fee](/glossary/gas)
|
||||
paid for by a third party instead of the transaction sender.
|
||||
Typically, this means the dApp owner covers the gas costs of their users' transactions, providing a more seamless user experience.
|
||||
|
||||
This works by having the user sign a message that is then sent to a relayer, which pays the gas fee and forwards the transaction to the blockchain.
|
||||
|
||||
You can attract a broader audience by removing the need to have gas to use your application. Engine offers multiple options for developers looking to sponsor gas for their users' transactions.
|
||||
|
||||
<Stack>
|
||||
|
||||
<ArticleIconCard
|
||||
title="Gasless transactions with Engine"
|
||||
href="/engine/features/gasless-transactions"
|
||||
icon={GraduationCap}
|
||||
/>
|
||||
|
||||
</Stack>
|
||||
@@ -1,5 +0,0 @@
|
||||
# Interoperability
|
||||
|
||||
Interoperability refers to the ability of different networks to share data or assets (like tokens) and interact with each other.
|
||||
|
||||
For example, if Blockchain A is interoperable with Blockchain B, it means that a user could send tokens from Blockchain A to Blockchain B in a way that's secure and verifiable on both chains.
|
||||
@@ -1,14 +0,0 @@
|
||||
# IPFS
|
||||
|
||||
IPFS (InterPlanetary File System) is a protocol to store files in a decentralized peer-to-peer network.
|
||||
|
||||
Storing information on the blockchain is expensive and comes with significant size limitations,
|
||||
IPFS provides a way to store files in a decentralized manner _not_ on the blockchain, sometimes referred to as "off-chain" storage.
|
||||
|
||||
To secure content on IPFS forever, a node must [pin](https://docs.ipfs.tech/concepts/persistence/) the content, otherwise, it may be discarded and therefore made unavailable.
|
||||
[Learn more about how IPFS works](https://ipfs.tech/#how).
|
||||
|
||||
To read data from IPFS, an [IPFS Gateway](https://docs.ipfs.tech/concepts/ipfs-gateway/) is required. This allows you to access data from the IPFS protocol on browsers and other HTTP clients, such
|
||||
as when building an application using our [SDK](/contracts).
|
||||
|
||||
Out of the box, all of our tools use [Storage](/storage) to store, pin, and retrieve data from IPFS via a gateway, currently powered by [Pinata](https://pinata.cloud/) under the hood.
|
||||
@@ -1,12 +0,0 @@
|
||||
import { DocLayout } from "@/components/Layouts/DocLayout";
|
||||
import { GlossaryBreadcrumb } from "./_components/GlossaryBreadcrumb";
|
||||
import { sidebar } from "./sidebar";
|
||||
|
||||
export default async function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<DocLayout sideBar={sidebar} editPageButton={true}>
|
||||
<GlossaryBreadcrumb sidebar={sidebar} />
|
||||
{props.children}
|
||||
</DocLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# Lazy Minting
|
||||
|
||||
Lazy minting is the preparation of NFT metadata without actually minting the NFT to a wallet address.
|
||||
|
||||
This is a useful process for smart contract admins who want to prepare metadata for NFTs that will be minted by other wallets in a [drop](/glossary/drop),
|
||||
_without_ paying the [gas cost](/glossary/gas) to mint those NFTs.
|
||||
|
||||
The typical process for lazy minting is:
|
||||
|
||||
1. Contract admin lazy-mints NFT metadata (either in bulk or for individual NFTs).
|
||||
2. Contract admin defines how NFTs can be minted using [claim phases](/glossary/claim-phases).
|
||||
3. Other wallets mint NFTs by claiming them. When they claim, they pay the gas cost to mint the NFT and the NFT is minted to their wallet address.
|
||||
@@ -1,10 +0,0 @@
|
||||
# Local Wallet
|
||||
|
||||
A local wallet is a type of wallet that allows you to create wallets within your application or project.
|
||||
|
||||
It is a non-custodial solution that simplifies the onboarding process by offering a "checkout as guest" experience.
|
||||
This improves the user experience for web3 apps in two ways:
|
||||
|
||||
1. It enables non-web3 native users to get started easily without having to create a wallet with an external provider like MetaMask.
|
||||
2. It hides transaction confirmations from users. With a local wallet, you can generate wallets on the backend,
|
||||
and each app can have one wallet that is app-scoped.
|
||||
@@ -1,6 +0,0 @@
|
||||
# Minting
|
||||
|
||||
Minting is the process of creating new tokens on the blockchain. These tokens can be any type of asset, such as ERC-20, ERC-721 or ERC-1155 tokens.
|
||||
Minting is usually done by the creator of the token (the default admin), but can also be done by anyone who has been given [permission](/contracts/build/extensions/general/Permissions) to mint tokens by the creator.
|
||||
|
||||
Minting is enabled on contracts that implement a `mint` function.
|
||||
@@ -1,12 +0,0 @@
|
||||
# NFT
|
||||
|
||||
A Non-fungible token (NFT) is a type of cryptographic token that represents something unique;
|
||||
non-fungible tokens are thus not mutually interchangeable.
|
||||
It is defined by the [ERC-721 standard](https://eips.ethereum.org/EIPS/eip-721), which is a standard for creating NFTs on the Ethereum blockchain.
|
||||
This is in contrast to ERC-20 tokens, which are 1:1 exchangeable.
|
||||
|
||||
An analogy to highlight this difference is that an ERC-20 token is like a \$20 bill, where the value of Sally's \$20 bill is equivalent to the value of John's \$20 bill.
|
||||
Think of an NFT as a collector's item, like a rare baseball card. Each card, or NFT, is unique and has different values based on the player, the year it was made, its condition, and other factors.
|
||||
When you buy a rare baseball card, you often get a certificate of authenticity and know the history of the card — who owned it, when it was sold, etc. The same applies to NFTs, but this information is recorded on the blockchain, making it transparent and impossible to forge.
|
||||
An NFT contains information on the token's provenance. The entire history of that NFT,
|
||||
including its creation and any subsequent transactions, is recorded on the blockchain, providing proof of authenticity and ownership.
|
||||
@@ -1,5 +0,0 @@
|
||||
# Non-Custodial Wallet
|
||||
|
||||
A non-custodial wallet (also known as a self-custody wallet) is a wallet where the user has full control of their security & assets.
|
||||
The user is solely responsible for the security of their private key and therefore their funds.
|
||||
Examples of non-custodial wallets are Coinbase Wallet, Electron, and Ledger.
|
||||
@@ -1,46 +0,0 @@
|
||||
import { createMetadata } from "@doc";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Glossary",
|
||||
description:
|
||||
"This section aims to guide you through web3 concepts, including EIPs and thirdweb-specific innovations",
|
||||
image: {
|
||||
title: "thirdweb Glossary",
|
||||
icon: "thirdweb",
|
||||
},
|
||||
});
|
||||
|
||||
# Glossary
|
||||
|
||||
This section aims to guide you through web3 concepts, including [EIPs](https://eips.ethereum.org/) and
|
||||
thirdweb-specific innovations.
|
||||
|
||||
| Term | Description |
|
||||
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Claim phases](/glossary/claim-phases) | Claim phases or "claim conditions" allow you to configure various aspects of a drop; such as who can claim tokens, how many tokens will be released, the price of each token, release date, etc. |
|
||||
| [Claiming](/glossary/claiming) | Claiming is the process of transferring token ownership to the claimer's wallet.. |
|
||||
| [Composability](/glossary/composability) | Composability refers to the ability for different protocols and smart contracts to interact and build upon each other. |
|
||||
| [Delayed reveal](/glossary/delayed-reveal) | Delayed reveal is a feature that allows you to release NFTs in a drop with hidden content that can only be revealed at a later time. The owner of the smart contract (or a wallet with the appropriate permissions) can lazy-mint a batch of NFTs, along with placeholder metadata and a password. |
|
||||
| [Drop](/glossary/drop) | A drop smart contract allows the owner of the contract to lazy mint NFTs (unless it is an ERC20 drop), and have other users mint them, typically under the conditions defined in the smart contract’s claim phases, although these are not required. |
|
||||
| [Externally Owned Account](/glossary/externally-owned-account) | An Externally Owned Account (EOA) is an account controlled by the owner of the account's private key. |
|
||||
| [Factory Contract](/glossary/factory-contract) | A factory contract is a smart contract that is used to deploy other smart contracts. |
|
||||
| [Gas fees](/glossary/gas) | All transactions (operations that change information stored on the blockchain) require a gas fee, which is used to cover the costs of the computational resources to execute that transaction. |
|
||||
| [Gasless Transactions](/glossary/gasless-transactions) | Gasless transactions, also known as meta-transactions, are transactions that have the gas fee paid for by a third party instead of the transaction sender. Typically, this means the dApp owner covers the gas costs of their users' transactions, providing a more seamless user experience. |
|
||||
| [Interoperability](/glossary/interoperability) | Interoperability refers to the ability of different networks to share data or assets (like tokens) and interact with each other. |
|
||||
| [IPFS](/glossary/ipfs) | IPFS is a protocol to store files in a decentralized peer-to-peer network. |
|
||||
| [Lazy minting](/glossary/lazy-minting) | Lazy minting is the preparation of NFT metadata without actually minting the NFT to a wallet address. |
|
||||
| [Local Wallet](/glossary/local-wallet) | A local wallet is a type of wallet that allows you to create wallets within your application or project. |
|
||||
| [Minting](/glossary/minting) | Minting is the process of creating new tokens on the blockchain.. |
|
||||
| [NFT](/glossary/nft) | A Non-fungible token (NFT) is a type of cryptographic token that is not mutually interchangeable. |
|
||||
| [Non-custodial Wallet](/glossary/non-custodial-wallet) | A non-custodial wallet (also known as a self-custody wallet) is a wallet where the user is solely responsible for their private key. |
|
||||
| [Permissionless](/glossary/permissionless) | Anyone can interact with a smart contract without needing special permission or approval. |
|
||||
| [Proxy Contracts](/glossary/local-wallet) | A Proxy smart contract contains the [Storage](/storage) for an upgradable contract that allows you to modify its logic post-deployment. |
|
||||
| [RPC URL](/glossary/rpc) | An RPC URL is an endpoint that enables an application to communicate with a blockchain network. It is used to send requests (initiate transactions) and receive responses (read data) from the blockchain, enabling developers to build applications that interact with the blockchain. |
|
||||
| [Signature-based minting](/glossary/signature-based-minting) | Signature-based minting, or "on-demand minting" allows a smart contract admin to generate signatures that can be used by other wallets to allow them to mint tokens on your smart contract in real-time. |
|
||||
| [Smart Contract](/glossary/smart-contract) | A smart contract is a computer program, stored on the blockchain. |
|
||||
| [Smart Wallet](/glossary/smart-wallet) | A Smart Wallet is a wallet that is controlled by a smart contract following the [ERC-4337 specification](https://eips.ethereum.org/EIPS/eip-4337). |
|
||||
| [Soulbound tokens](/glossary/soulbound) | Soulbound tokens refer to NFTs that are permanently attached to a specific wallet address on the blockchain. |
|
||||
| [Staking](/glossary/staking) | Staking is the process of locking up tokens to support the operations of a blockchain network. |
|
||||
| [Token](/glossary/token) | A token is a type of digital asset that represents a particular value or utility. |
|
||||
| [Wallet](/glossary/wallet) | A wallet is a user's way of interacting with the blockchain.. |
|
||||
| [Bundler](/glossary/bundler) | A bundler is a node that monitors the alternative mempool of `UserOperations` and bundles multiple `UserOps` together to forward to the EntryPoint contract as a single transaction. |
|
||||
@@ -1,10 +0,0 @@
|
||||
# Paymaster
|
||||
|
||||
A paymaster is a smart contract that relays transactions.
|
||||
It provides a service that enables a third party to pay the transaction fee on behalf
|
||||
of the user by funding the paymaster in advance.
|
||||
The Paymaster acts as a gas reserve which then can be used during the call execution via the
|
||||
`EntryPoint` contract.
|
||||
|
||||
The thirdweb paymaster is initially paid for by thirdweb.
|
||||
By using an [API key](/api-keys), the usage is tracked and billed.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Permissionless
|
||||
|
||||
On the context of smart contracts, permissionless means that anyone can interact with the contract without needing special permission or approval.
|
||||
Smart contracts can be deployed on public blockchains like Ethereum,
|
||||
promoting open access and transparency. While anyone can interact with a permissionless smart contract,
|
||||
the contract's code may still limit certain operations (like modifying the contract or accessing certain data)
|
||||
to specific roles, such as the contract's owner or creator.
|
||||
@@ -1,12 +0,0 @@
|
||||
# Proxy Contracts
|
||||
|
||||
A Proxy smart contract contains the [Storage](/storage) for an upgradable contract that allows you to modify its logic post-deployment.
|
||||
|
||||
A smart contract is made up of (1) persistent state/storage, and (2) logic defined for how this state can be mutated. Upgrading a smart contract means changing the logic defined for how the smart contract’s state can be mutated.
|
||||
|
||||
An “upgradeable smart contract” is two smart contracts considered together as one system:
|
||||
|
||||
1. **Proxy smart contract**: The smart contract whose state/storage we’re concerned with.
|
||||
2. **Implementation smart contract**: A stateless smart contract that defines the logic for how the proxy smart contract’s state can be mutated.
|
||||
|
||||
To _upgrade_ the smart contract, simply deploy a new implementation contract and call the upgrade function on the proxy contract to _upgrade_ to the new implementation.
|
||||
@@ -1,8 +0,0 @@
|
||||
# RPC URLs
|
||||
|
||||
An RPC (Remote Procedure Call) URL
|
||||
is an endpoint that enables an application to communicate with a blockchain network.
|
||||
It is used to send requests (initiate transactions) and receive responses (read data) from the blockchain,
|
||||
enabling developers to build applications that interact with the blockchain.
|
||||
|
||||
Out of the box, we provide free [RPC URLs](https://thirdweb.com/chainlist) for all supported blockchains, including All EVM-compatible networks.
|
||||
@@ -1,115 +0,0 @@
|
||||
import type { SideBar } from "@/components/Layouts/DocLayout";
|
||||
|
||||
export const sidebar: SideBar = {
|
||||
name: "Glossary",
|
||||
links: [
|
||||
{
|
||||
name: "Claim Phases",
|
||||
href: "/glossary/claim-phases",
|
||||
},
|
||||
{
|
||||
name: "Claiming",
|
||||
href: "/glossary/claiming",
|
||||
},
|
||||
{
|
||||
href: "/glossary/composability",
|
||||
name: "Composability",
|
||||
},
|
||||
{
|
||||
href: "/glossary/delayed-reveal",
|
||||
name: "Delayed Reveal",
|
||||
},
|
||||
{
|
||||
href: "/glossary/drop",
|
||||
name: "Drop",
|
||||
},
|
||||
{
|
||||
name: "Externally Owned Account",
|
||||
href: "/glossary/externally-owned-account",
|
||||
},
|
||||
{
|
||||
name: "Factory Contract",
|
||||
href: "/glossary/factory-contract",
|
||||
},
|
||||
{
|
||||
name: "Gas fees",
|
||||
href: "/glossary/gas",
|
||||
},
|
||||
{
|
||||
name: "Gasless Transactions",
|
||||
href: "/glossary/gasless-transactions",
|
||||
},
|
||||
{
|
||||
name: "Interoperability",
|
||||
href: "/glossary/interoperability",
|
||||
},
|
||||
{
|
||||
name: "IPFS",
|
||||
href: "/glossary/ipfs",
|
||||
},
|
||||
{
|
||||
name: "Lazy minting",
|
||||
href: "/glossary/lazy-minting",
|
||||
},
|
||||
{
|
||||
name: "Local Wallet",
|
||||
href: "/glossary/local-wallet",
|
||||
},
|
||||
{
|
||||
name: "Minting",
|
||||
href: "/glossary/minting",
|
||||
},
|
||||
{
|
||||
name: "NFT",
|
||||
href: "/glossary/nft",
|
||||
},
|
||||
{
|
||||
name: "Non-custodial Wallet",
|
||||
href: "/glossary/non-custodial-wallet",
|
||||
},
|
||||
{
|
||||
name: "Permissionless",
|
||||
href: "/glossary/permissionless",
|
||||
},
|
||||
{
|
||||
name: "Proxy Contracts",
|
||||
href: "/glossary/proxy-contracts",
|
||||
},
|
||||
{
|
||||
name: "RPC URL",
|
||||
href: "/glossary/rpc",
|
||||
},
|
||||
{
|
||||
name: "Signature-based minting",
|
||||
href: "/glossary/signature-based-minting",
|
||||
},
|
||||
{
|
||||
name: "Smart Contract",
|
||||
href: "/glossary/smart-contract",
|
||||
},
|
||||
{
|
||||
name: "Smart Account",
|
||||
href: "/glossary/smart-account",
|
||||
},
|
||||
{
|
||||
name: "Soulbound tokens",
|
||||
href: "/glossary/soulbound",
|
||||
},
|
||||
{
|
||||
name: "Staking",
|
||||
href: "/glossary/staking",
|
||||
},
|
||||
{
|
||||
name: "Token",
|
||||
href: "/glossary/token",
|
||||
},
|
||||
{
|
||||
name: "Wallet",
|
||||
href: "/glossary/wallet",
|
||||
},
|
||||
{
|
||||
name: "Bundler",
|
||||
href: "/glossary/bundler",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
# Signature-based Minting
|
||||
|
||||
Signature-based minting, or "on-demand minting" allows a smart contract admin to generate signatures
|
||||
that can be used by other wallets to allow them to mint tokens on your smart contract in real-time.
|
||||
|
||||
This allows you to conditionally generate signatures based on some condition/criteria, or allow a user to mint
|
||||
NFTs with metadata that you specify. Enabling use cases such as:
|
||||
|
||||
- [Allowing NFT holders to mint for free](https://blog.thirdweb.com/guides/signature-drop/)
|
||||
- [Creating a community-made NFT collection](https://blog.thirdweb.com/guides/mint-nft-unique-code/)
|
||||
- [Restricted mints to users who are GitHub contributors](https://blog.thirdweb.com/guides/allow-github-contributors-to-mint-an-nft/)
|
||||
- [Restricted mints to Discord members](https://github.com/thirdweb-example/community-rewards)
|
||||
@@ -1,13 +0,0 @@
|
||||
# Smart Contract
|
||||
|
||||
A smart contract is a computer program, stored on the blockchain.
|
||||
Smart contracts allow trustless transactions to be executed without third parties - the smart contract enforces the rules of the transaction automatically.
|
||||
These transactions are trackable and irreversible.
|
||||
|
||||
Key features of smart contracts are:
|
||||
|
||||
- **Autonomous**: Once a smart contract is initiated, it can act by itself without further participant involvement.
|
||||
|
||||
- **Decentralized**: No single party has control over the entirety of the contract, and it cannot be modified after deployment.
|
||||
|
||||
- **Deterministic**: Outcomes can be determined from the inputs and the contract code.
|
||||
@@ -1,55 +0,0 @@
|
||||
# Smart Account
|
||||
|
||||
A Smart Account is a wallet that is controlled by a smart contract following the [ERC-4337 specification](https://eips.ethereum.org/EIPS/eip-4337).
|
||||
|
||||
Ethereum has 2 types of accounts to transfer and receive tokens: EOAs (Externally Owned Accounts) and Contract Accounts. A smart contract wallet is a wallet managed by a contract account
|
||||
instead of an EOA. A smart account is a wallet type requiring **no private keys or seed phrases**.
|
||||
Smart contract wallets rely on code instead of private keys to secure and recover wallet information.
|
||||
Instead, you set _who_ and _under what conditions_, users may access the wallet via smart contracts.
|
||||
|
||||
**A smart contract wallet is a type of Web3 wallet powered by smart contracts.**
|
||||
|
||||
This smart account is unlocked by a 'key' - a personal account. This key can be anything from a [MetaMask wallet](/typescript/v5/supported-wallets/io.metamask) or even an [in-App Wallet](/typescript/v5/in-app-wallet) and is used as a way to 'sign in' to the wallet.
|
||||
|
||||
Benefits of Smart Accounts:
|
||||
|
||||
1. Optimized transaction efficiency & batch transactions
|
||||
- Combine multiple transactions into a single transaction to save on [gas fees](/glossary/gas)
|
||||
2. Improved Security Features
|
||||
3. Social & Multi-Sig Recovery
|
||||
- Recover a wallet without seed phrases/passwords
|
||||
- Nominate a trusted person to recover your wallet in the case of an emergency
|
||||
|
||||
## Terminology
|
||||
|
||||
### UserOperations
|
||||
|
||||
This is the data structure for the transaction that the user wants to perform. It contains the following fields:
|
||||
|
||||
- `sender`: The account making the operation.
|
||||
- `nonce`: Anti-replay parameter; also used as the salt for first-time account creation.
|
||||
- `initCode`: The initialization code needed to create the account (needed if and only if the account is not yet onchain).
|
||||
- `callData`: The data to pass to the `sender` during the operation.
|
||||
- `callGasLimit`: The amount of gas to allocate for the operation.
|
||||
- `verificationGasLimit`: The amount of gas to allocate for the verification step.
|
||||
- `preVerificationGas`: The amount of gas to pay for to compensate the bundler for pre-verification execution and calldata.
|
||||
- `maxFeePerGas`: Maximum fee per gas (similar to [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) `max_fee_per_gas`).
|
||||
- `maxPriorityFeePerGas`: Maximum priority fee per gas (similar to [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)).
|
||||
- `paymasterAndData`: Address of paymaster sponsoring the transaction, followed by extra data to send to the paymaster (empty for self-sponsored transaction).
|
||||
- `signature`: Data passed into the account along with the nonce during the verification step. Should depend on the `chainid` and `EntryPoint` address to prevent replay attacks.
|
||||
|
||||
### EntryPoint
|
||||
|
||||
The EntryPoint is a singleton contract (a contract that has a predictable address that is the same on every chain).
|
||||
It has two methods that are used as entry points to execute bundles of UserOperations: `handleOps` and `handleAggregatedOps`.
|
||||
|
||||
### Bundler (relayer)
|
||||
|
||||
A bundler is a smart contract that bundles multiple 'UserOperations' into a single bundle, a `handleOps` call, to the `EntryPoint` contract.
|
||||
|
||||
### Paymaster
|
||||
|
||||
A paymaster is a smart contract that relays transactions. It provides a service that enables a third party to pay
|
||||
the transaction fee on behalf of the user by funding the paymaster in advance. The Paymaster acts as a gas reserve which then
|
||||
can be used during the call execution via `EntryPoint`. The thirdweb [Account ABstraction](/connect/account-abstraction) paymaster is initially paid for by thirdweb. By using an API key,
|
||||
the usage is tracked and billed.
|
||||
@@ -1,6 +0,0 @@
|
||||
# Soulbound Tokens
|
||||
|
||||
Soulbound tokens refer to NFTs
|
||||
that are permanently attached to a specific wallet address on the blockchain.
|
||||
|
||||
Once a soulbound token is created, it cannot be transferred, sold, or traded to another address.
|
||||
@@ -1,9 +0,0 @@
|
||||
# Staking
|
||||
|
||||
Staking is the process of locking up tokens to support the operations of a blockchain network.
|
||||
In return for staking, users receive rewards in the form of additional tokens.
|
||||
Staking is a key element of Proof of Stake (PoS) consensus mechanisms, which are used by many blockchains to secure their networks.
|
||||
|
||||
When you stake your tokens, you essentially lock them up in the network for a certain period. Depending on the specific protocol, staking your tokens might allow you to participate in network governance (voting on various decisions), validate transactions, and earn rewards.
|
||||
|
||||
Staking serves the dual purpose of incentivizing participants to operate in the best interest of the network (since they have a financial stake in it) and deter malicious activity (since malicious actors stand to lose their staked tokens).
|
||||
@@ -1,5 +0,0 @@
|
||||
# Token
|
||||
|
||||
A token is a type of digital asset that represents a particular value or utility. Tokens are defined by smart contracts which follow a token standard such as the ERC-20, ERC-1155 or ERC-721 standards.
|
||||
|
||||
Tokens are a critical component of decentralized applications (dApps) built on top of blockchain platforms. They can incentivize certain behaviors, represent ownership of digital or physical assets, or facilitate transactions within the dApp's ecosystem.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Wallet
|
||||
|
||||
A wallet is a user's way of interacting with the blockchain. Wallets can be used to store, send, and receive blockchain assets such as ERC-20 tokens and NFTS.
|
||||
@@ -1,5 +1,13 @@
|
||||
# @thirdweb-dev/service-utils
|
||||
|
||||
## 0.9.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6837](https://github.com/thirdweb-dev/js/pull/6837) [`d95b3d3`](https://github.com/thirdweb-dev/js/commit/d95b3d38c721797e7470c198abb44edf1a1f6495) Thanks [@arcoraven](https://github.com/arcoraven)! - chore: Add unthreadCustomerId to TeamResponse
|
||||
|
||||
- [#6842](https://github.com/thirdweb-dev/js/pull/6842) [`f2d0f11`](https://github.com/thirdweb-dev/js/commit/f2d0f119c1422eebb37c898e30832d5fda33f5f6) Thanks [@jnsdls](https://github.com/jnsdls)! - add insight.webhooks to capabilities
|
||||
|
||||
## 0.9.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/service-utils",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.6",
|
||||
"type": "module",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
|
||||
@@ -55,6 +55,7 @@ type TeamCapabilities = {
|
||||
insight: {
|
||||
enabled: boolean;
|
||||
rateLimit: number;
|
||||
webhooks: boolean;
|
||||
};
|
||||
storage: {
|
||||
enabled: boolean;
|
||||
@@ -123,6 +124,7 @@ export type TeamResponse = {
|
||||
enabledScopes: ServiceName[];
|
||||
isOnboarded: boolean;
|
||||
capabilities: TeamCapabilities;
|
||||
unthreadCustomerId: string | null;
|
||||
planCancellationDate: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ export const validTeamResponse: TeamResponse = {
|
||||
insight: {
|
||||
enabled: true,
|
||||
rateLimit: 1000,
|
||||
webhooks: true,
|
||||
},
|
||||
storage: {
|
||||
enabled: true,
|
||||
@@ -99,6 +100,7 @@ export const validTeamResponse: TeamResponse = {
|
||||
},
|
||||
},
|
||||
planCancellationDate: null,
|
||||
unthreadCustomerId: null,
|
||||
};
|
||||
|
||||
export const validTeamAndProjectResponse: TeamAndProjectResponse = {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.96.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6834](https://github.com/thirdweb-dev/js/pull/6834) [`4c33936`](https://github.com/thirdweb-dev/js/commit/4c339363c0480b0ba3994355d28ac7a176930edc) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Always checksum Account.address
|
||||
|
||||
## 5.96.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6832](https://github.com/thirdweb-dev/js/pull/6832) [`436750e`](https://github.com/thirdweb-dev/js/commit/436750ed15cda8d646fc60514a6a17fca953d445) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Lazy import native dependencies in React Native
|
||||
|
||||
## 5.96.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.96.0",
|
||||
"version": "5.96.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TransactionTypeMap,
|
||||
} from "../transaction/prepare-transaction.js";
|
||||
import type { SerializableTransaction } from "../transaction/serialize-transaction.js";
|
||||
import { getAddress } from "../utils/address.js";
|
||||
import { toHex } from "../utils/encoding/hex.js";
|
||||
import type { Account } from "../wallets/interfaces/wallet.js";
|
||||
|
||||
@@ -364,7 +365,7 @@ export async function fromEthersSigner(
|
||||
): Promise<Account> {
|
||||
const address = await signer.getAddress();
|
||||
const account: Account = {
|
||||
address,
|
||||
address: getAddress(address),
|
||||
signMessage: async ({ message }) => {
|
||||
return signer.signMessage(
|
||||
typeof message === "string" ? message : message.raw,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { type ThirdwebContract, getContract } from "../contract/contract.js";
|
||||
import { toSerializableTransaction } from "../transaction/actions/to-serializable-transaction.js";
|
||||
import type { PreparedTransaction } from "../transaction/prepare-transaction.js";
|
||||
import type { SerializableTransaction } from "../transaction/serialize-transaction.js";
|
||||
import { getAddress } from "../utils/address.js";
|
||||
import { toHex } from "../utils/encoding/hex.js";
|
||||
import { resolvePromisedValue } from "../utils/promise/resolve-promised-value.js";
|
||||
import type { Account } from "../wallets/interfaces/wallet.js";
|
||||
@@ -327,7 +328,7 @@ export async function fromEthersSigner(
|
||||
): Promise<Account> {
|
||||
const address = await signer.getAddress();
|
||||
const account: Account = {
|
||||
address,
|
||||
address: getAddress(address),
|
||||
signMessage: async ({ message }) => {
|
||||
return signer.signMessage(
|
||||
typeof message === "string" ? message : message.raw,
|
||||
|
||||
@@ -284,7 +284,7 @@ function createAccount({
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const account: Account = {
|
||||
address,
|
||||
address: getAddress(address),
|
||||
async sendTransaction(tx: SendTransactionOption) {
|
||||
const transactionHash = (await provider.request({
|
||||
method: "eth_sendTransaction",
|
||||
|
||||
@@ -3,22 +3,7 @@ import { stringify } from "../../../utils/json.js";
|
||||
import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js";
|
||||
import { nativeLocalStorage } from "../../../utils/storage/nativeStorage.js";
|
||||
import type { Account } from "../../interfaces/wallet.js";
|
||||
import { getUserStatus } from "../core/actions/get-enclave-user-status.js";
|
||||
import { authEndpoint } from "../core/authentication/authEndpoint.js";
|
||||
import { backendAuthenticate } from "../core/authentication/backend.js";
|
||||
import { ClientScopedStorage } from "../core/authentication/client-scoped-storage.js";
|
||||
import { guestAuthenticate } from "../core/authentication/guest.js";
|
||||
import { customJwt } from "../core/authentication/jwt.js";
|
||||
import {
|
||||
getLinkedProfilesInternal,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
} from "../core/authentication/linkAccount.js";
|
||||
import {
|
||||
loginWithPasskey,
|
||||
registerPasskey,
|
||||
} from "../core/authentication/passkeys.js";
|
||||
import { siweAuthenticate } from "../core/authentication/siwe.js";
|
||||
import type {
|
||||
AuthArgsType,
|
||||
AuthLoginReturnType,
|
||||
@@ -31,13 +16,9 @@ import type {
|
||||
SingleStepAuthArgsType,
|
||||
} from "../core/authentication/types.js";
|
||||
import type { InAppConnector } from "../core/interfaces/connector.js";
|
||||
import { EnclaveWallet } from "../core/wallet/enclave-wallet.js";
|
||||
import type { Ecosystem } from "../core/wallet/types.js";
|
||||
import type { IWebWallet } from "../core/wallet/web-wallet.js";
|
||||
import { sendOtp, verifyOtp } from "../web/lib/auth/otp.js";
|
||||
import { deleteActiveAccount, socialAuth } from "./auth/native-auth.js";
|
||||
import { logoutUser } from "./helpers/auth/logout.js";
|
||||
import { ShardedWallet } from "./helpers/wallet/sharded-wallet.js";
|
||||
|
||||
type NativeConnectorOptions = {
|
||||
client: ThirdwebClient;
|
||||
ecosystem?: Ecosystem;
|
||||
@@ -73,6 +54,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
"No auth token provided and no stored auth token found to initialize the wallet",
|
||||
);
|
||||
}
|
||||
const { getUserStatus } = await import(
|
||||
"../core/actions/get-enclave-user-status.js"
|
||||
);
|
||||
const user = await getUserStatus({
|
||||
authToken:
|
||||
authResult?.storedToken.cookieString || (storedAuthToken as string),
|
||||
@@ -115,6 +99,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
}
|
||||
|
||||
if (wallet && wallet.type === "enclave") {
|
||||
const { EnclaveWallet } = await import(
|
||||
"../core/wallet/enclave-wallet.js"
|
||||
);
|
||||
this.wallet = new EnclaveWallet({
|
||||
client: this.client,
|
||||
ecosystem: this.ecosystem,
|
||||
@@ -122,6 +109,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
storage: this.storage,
|
||||
});
|
||||
} else {
|
||||
const { ShardedWallet } = await import(
|
||||
"./helpers/wallet/sharded-wallet.js"
|
||||
);
|
||||
this.wallet = new ShardedWallet({
|
||||
client: this.client,
|
||||
storage: this.storage,
|
||||
@@ -150,7 +140,8 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
return this.wallet.getAccount();
|
||||
}
|
||||
|
||||
preAuthenticate(args: MultiStepAuthProviderType): Promise<void> {
|
||||
async preAuthenticate(args: MultiStepAuthProviderType): Promise<void> {
|
||||
const { sendOtp } = await import("../web/lib/auth/otp.js");
|
||||
return sendOtp({
|
||||
...args,
|
||||
client: this.client,
|
||||
@@ -164,9 +155,13 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
switch (strategy) {
|
||||
case "email":
|
||||
case "phone": {
|
||||
const { verifyOtp } = await import("../web/lib/auth/otp.js");
|
||||
return verifyOtp(params);
|
||||
}
|
||||
case "guest": {
|
||||
const { guestAuthenticate } = await import(
|
||||
"../core/authentication/guest.js"
|
||||
);
|
||||
return guestAuthenticate({
|
||||
client: this.client,
|
||||
ecosystem: params.ecosystem,
|
||||
@@ -174,6 +169,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
});
|
||||
}
|
||||
case "backend": {
|
||||
const { backendAuthenticate } = await import(
|
||||
"../core/authentication/backend.js"
|
||||
);
|
||||
return backendAuthenticate({
|
||||
client: this.client,
|
||||
walletSecret: params.walletSecret,
|
||||
@@ -181,6 +179,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
});
|
||||
}
|
||||
case "wallet": {
|
||||
const { siweAuthenticate } = await import(
|
||||
"../core/authentication/siwe.js"
|
||||
);
|
||||
return siweAuthenticate({
|
||||
client: this.client,
|
||||
wallet: params.wallet,
|
||||
@@ -199,6 +200,7 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
case "line":
|
||||
case "x":
|
||||
case "apple": {
|
||||
const { socialAuth } = await import("./auth/native-auth.js");
|
||||
const ExpoLinking = require("expo-linking");
|
||||
const redirectUrl =
|
||||
params.redirectUrl || (ExpoLinking.createURL("") as string);
|
||||
@@ -210,18 +212,24 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
}
|
||||
case "passkey":
|
||||
return this.passkeyAuth(params);
|
||||
case "jwt":
|
||||
case "jwt": {
|
||||
const { customJwt } = await import("../core/authentication/jwt.js");
|
||||
return customJwt({
|
||||
jwt: params.jwt,
|
||||
client: this.client,
|
||||
ecosystem: this.ecosystem,
|
||||
});
|
||||
case "auth_endpoint":
|
||||
}
|
||||
case "auth_endpoint": {
|
||||
const { authEndpoint } = await import(
|
||||
"../core/authentication/authEndpoint.js"
|
||||
);
|
||||
return authEndpoint({
|
||||
payload: params.payload,
|
||||
client: this.client,
|
||||
ecosystem: this.ecosystem,
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported authentication type: ${strategy}`);
|
||||
}
|
||||
@@ -287,6 +295,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
let authToken: AuthStoredTokenWithCookieReturnType;
|
||||
|
||||
if (type === "sign-up") {
|
||||
const { registerPasskey } = await import(
|
||||
"../core/authentication/passkeys.js"
|
||||
);
|
||||
authToken = await registerPasskey({
|
||||
client,
|
||||
ecosystem,
|
||||
@@ -299,6 +310,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const { loginWithPasskey } = await import(
|
||||
"../core/authentication/passkeys.js"
|
||||
);
|
||||
authToken = await loginWithPasskey({
|
||||
client,
|
||||
ecosystem,
|
||||
@@ -325,13 +339,15 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
|
||||
// TODO (rn) expose in the interface
|
||||
async deleteActiveAccount() {
|
||||
const { deleteActiveAccount } = await import("./auth/native-auth.js");
|
||||
return deleteActiveAccount({
|
||||
client: this.client,
|
||||
storage: this.storage,
|
||||
});
|
||||
}
|
||||
|
||||
logout(): Promise<LogoutReturnType> {
|
||||
async logout(): Promise<LogoutReturnType> {
|
||||
const { logoutUser } = await import("./helpers/auth/logout.js");
|
||||
return logoutUser({
|
||||
client: this.client,
|
||||
storage: this.storage,
|
||||
@@ -339,6 +355,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
}
|
||||
|
||||
async linkProfile(args: AuthArgsType) {
|
||||
const { linkAccount } = await import(
|
||||
"../core/authentication/linkAccount.js"
|
||||
);
|
||||
const { storedToken } = await this.authenticate(args);
|
||||
return await linkAccount({
|
||||
client: args.client,
|
||||
@@ -349,6 +368,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
}
|
||||
|
||||
async unlinkProfile(profile: Profile) {
|
||||
const { unlinkAccount } = await import(
|
||||
"../core/authentication/linkAccount.js"
|
||||
);
|
||||
return await unlinkAccount({
|
||||
client: this.client,
|
||||
ecosystem: this.ecosystem,
|
||||
@@ -358,6 +380,9 @@ export class InAppNativeConnector implements InAppConnector {
|
||||
}
|
||||
|
||||
async getProfiles() {
|
||||
const { getLinkedProfilesInternal } = await import(
|
||||
"../core/authentication/linkAccount.js"
|
||||
);
|
||||
return getLinkedProfilesInternal({
|
||||
client: this.client,
|
||||
ecosystem: this.ecosystem,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getRpcClient } from "../rpc/rpc.js";
|
||||
import type { AuthorizationRequest } from "../transaction/actions/eip7702/authorization.js";
|
||||
import { signTransaction } from "../transaction/actions/sign-transaction.js";
|
||||
import type { SerializableTransaction } from "../transaction/serialize-transaction.js";
|
||||
import { getAddress } from "../utils/address.js";
|
||||
import { type Hex, toHex } from "../utils/encoding/hex.js";
|
||||
import { signMessage } from "../utils/signatures/sign-message.js";
|
||||
import { signTypedData } from "../utils/signatures/sign-typed-data.js";
|
||||
@@ -79,7 +80,7 @@ export function privateKeyToAccount(
|
||||
const address = publicKeyToAddress(publicKey);
|
||||
|
||||
const account = {
|
||||
address,
|
||||
address: getAddress(address),
|
||||
sendTransaction: async (
|
||||
tx: SerializableTransaction & { chainId: number },
|
||||
) => {
|
||||
|
||||
@@ -421,7 +421,7 @@ function createZkSyncAccount(args: {
|
||||
}): Account {
|
||||
const { creationOptions, connectionOptions, chain } = args;
|
||||
const account: Account = {
|
||||
address: connectionOptions.personalAccount.address,
|
||||
address: getAddress(connectionOptions.personalAccount.address),
|
||||
async sendTransaction(transaction: SendTransactionOption) {
|
||||
// override passed tx, we have to refetch gas and fees always
|
||||
const prepTx = {
|
||||
|
||||
@@ -333,7 +333,7 @@ function createAccount({
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const account: Account = {
|
||||
address: address,
|
||||
address: getAddress(address),
|
||||
async sendTransaction(tx: SendTransactionOption) {
|
||||
const transactionHash = (await provider.request({
|
||||
method: "eth_sendTransaction",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.62
|
||||
|
||||
## 0.2.61
|
||||
|
||||
## 0.2.60
|
||||
|
||||
## 0.2.59
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/wagmi-adapter",
|
||||
"version": "0.2.60",
|
||||
"version": "0.2.62",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
Reference in New Issue
Block a user