Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d617bf77b3 Version Packages (#7851)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-12 18:39:30 -07:00
Joaquim VergesandGitHub 4be655dd08 [SDK] Fix onDisconnect callback not being invoked in React Native (#7850) 2025-08-12 18:18:45 -07:00
MananTank 74d4074f42 Dashboard: Add fallback to native token balance on contract/split page when insight is not supported (#7847)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the handling of token balances and recipient percentages in the `ContractSplitPage` and related components, enhancing user experience and error handling.

### Detailed summary
- Updated `transactionCount` logic in `DistributeButton` to handle undefined cases.
- Added error handling for fetching token balances using `tryCatch`.
- Improved UI structure and styling in `ContractSplitPage`.
- Changed variable naming for clarity in `useReadContract`.
- Enhanced loading states for recipient percentages and balances.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved reliability of token balances by falling back to native balance if token data fails to load.
* **Style**
  * Refreshed Split page layout: simplified headers, refined spacing, and updated table containers.
  * Adjusted column widths for Tokens and Addresses for better readability.
  * Enhanced loading skeletons and maintained clear empty/error states.
* **Refactor**
  * Streamlined data flow for recipients and balances to use a unified query object.
* **Chores**
  * Button tweaks: small size on error; hide transaction count when only one transaction.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-12 23:41:08 +00:00
MananTank a36c850b86 [BLD-108] Dashboard: Update Claim conditions UI to handle very large snapshots (#7846)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the handling of CSV uploads and snapshots in the dashboard, improving user feedback during data processing, and refining the management of claim conditions within the application.

### Detailed summary
- Updated `package.json` to remove `p-limit`.
- Improved UI for CSV upload status with additional messages.
- Refactored snapshot management in `ClaimConditionsForm`.
- Added state for `phaseSnapshots` to optimize performance.
- Enhanced snapshot handling in `SnapshotUpload` component.
- Updated data processing logic in `useCsvUpload` to track progress.

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

<!-- end pr-codex -->

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

- New Features
  - CSV upload shows real-time normalization progress and ENS resolution status across relevant upload screens; pending states now display current/total progress.
  - Per-phase snapshot management with centralized state; improved snapshot viewer with view/edit modes, pagination, and clearer labels.
  - Improved validation: invalid addresses highlighted with tooltips and options to remove or proceed.

- Refactor
  - CSV normalization switched to batched processing for smoother, more responsive uploads.
  - Snapshot data moved out of per-field form state to reduce rendering overhead.

- Chores
  - Removed an unused dependency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-12 22:55:13 +00:00
59a8dad48e [Playground] ai chat demo (#7833)
Co-authored-by: Cursor Agent <[email protected]>
2025-08-12 15:17:03 -07:00
MananTank f7aa3e6239 [BLD-80] Dashboard: in-app wallet settings page UI improvements (#7843)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the UI components and improving the country selection functionality in the dashboard application. It includes updates to transitions, checkbox indicators, and country names for better clarity and usability.

### Detailed summary
- Updated transition duration in `DynamicHeight.tsx`.
- Added `MinusIcon` for indeterminate state in `checkbox.tsx`.
- Corrected country names in `utils.ts` for several countries.
- Improved structure and styling of the `CountrySelector` component.
- Added `ChevronDownIcon` and `Button` for toggling tiers in `country-selector.tsx`.
- Refactored `Fieldset` and `FieldsetWithDescription` components for better layout and usability in settings forms.

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

<!-- end pr-codex -->

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

- New Features
  - Checkbox now supports an indeterminate state with a clear visual indicator.
  - Wallet settings sections gain per-section Save buttons with loading spinners.
  - SMS country selection adds collapsible tier cards, partial-selection counters, and improved country tiles with clear selection states.

- Refactor
  - Unified fieldset layouts with consistent headers, descriptions, dividers, and footers.
  - Branding settings reorganized into a responsive two‑column layout with improved flow.
  - Country tier rendering moved to modular TierCard components.

- Bug Fixes
  - Corrected several country display names (e.g., South Korea, North Macedonia, Eswatini, Saint Lucia).

- Style
  - Adjusted a height-transition timing/easing for dynamic-height animations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-12 21:41:49 +00:00
37 changed files with 2865 additions and 612 deletions
-1
View File
@@ -43,7 +43,6 @@
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"p-limit": "^6.2.0",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
@@ -17,9 +17,7 @@ export function DynamicHeight(props: {
boxSizing: "border-box",
height: height ? `${height}px` : "auto",
overflow: "hidden",
transition:
props.transition ||
"height 210ms cubic-bezier(0.175, 0.885, 0.32, 1.1)",
transition: props.transition || "height 250ms ease",
}}
>
<div
@@ -1,7 +1,7 @@
"use client";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { CheckIcon, MinusIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -21,7 +21,11 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="size-4" />
{props.checked === "indeterminate" ? (
<MinusIcon className="size-3" />
) : (
<CheckIcon className="size-4" />
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
+34 -14
View File
@@ -1,5 +1,4 @@
import { useQuery } from "@tanstack/react-query";
import pLimit from "p-limit";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import Papa from "papaparse";
import { useCallback, useState } from "react";
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
@@ -95,6 +94,7 @@ export function useCsvUpload<
// Always gonna need the wallet address
T extends { address: string },
>(props: Props<T>) {
const queryClient = useQueryClient();
const [rawData, setRawData] = useState<
T[] | Array<T & { [key in string]: unknown }>
>(props.defaultRawData || []);
@@ -132,16 +132,29 @@ export function useCsvUpload<
[props.csvParser],
);
const [normalizeProgress, setNormalizeProgress] = useState({
total: 0,
current: 0,
});
const normalizeQuery = useQuery({
queryFn: async () => {
const limit = pLimit(50);
const results = await Promise.all(
rawData.map((item) => {
return limit(() =>
const batchSize = 50;
const results = [];
for (let i = 0; i < rawData.length; i += batchSize) {
const batch = rawData.slice(i, i + batchSize);
setNormalizeProgress({
total: rawData.length,
current: i,
});
const batchResults = await Promise.all(
batch.map((item) =>
checkIsAddress({ item: item, thirdwebClient: props.client }),
);
}),
);
),
);
results.push(...batchResults);
}
return {
invalidFound: !!results.find((item) => !item?.isValid),
result: processAirdropData(results),
@@ -153,12 +166,18 @@ export function useCsvUpload<
const removeInvalid = useCallback(() => {
const filteredData = normalizeQuery.data?.result.filter(
({ isValid }) => isValid,
(d) => d.isValid && d.resolvedAddress !== ZERO_ADDRESS,
);
// double type assertion is save here because we don't really use this variable (only check for its length)
// Also filteredData's type is the superset of T[]
setRawData(filteredData as unknown as T[]);
}, [normalizeQuery.data?.result]);
if (filteredData && normalizeQuery.data) {
// Directly update the query result instead of setting new state to avoid triggering refetch
queryClient.setQueryData(["snapshot-check-isAddress", rawData], {
...normalizeQuery.data,
result: filteredData,
invalidFound: false, // Since we removed all invalid items
});
}
}, [normalizeQuery.data, queryClient, rawData]);
const processData = useCallback(
(data: T[]) => {
@@ -181,5 +200,6 @@ export function useCsvUpload<
removeInvalid,
reset,
setFiles,
normalizeProgress,
};
}
+30 -1
View File
@@ -7,15 +7,19 @@ import {
import { toast } from "sonner";
import {
type Chain,
NATIVE_TOKEN_ADDRESS,
sendAndConfirmTransaction,
type ThirdwebClient,
type ThirdwebContract,
} from "thirdweb";
import type { GetBalanceResult } from "thirdweb/extensions/erc20";
import { distribute, distributeByToken } from "thirdweb/extensions/split";
import { getOwnedTokens } from "thirdweb/insight";
import { useActiveAccount } from "thirdweb/react";
import { getWalletBalance } from "thirdweb/wallets";
import invariant from "tiny-invariant";
import { parseError } from "../utils/errorParser";
import { tryCatch } from "../utils/try-catch";
function getTokenBalancesQuery(params: {
ownerAddress: string;
@@ -24,7 +28,7 @@ function getTokenBalancesQuery(params: {
}) {
return queryOptions({
queryFn: async () => {
return getOwnedTokens({
const ownedTokenBalancePromise = getOwnedTokens({
client: params.client,
chains: [params.chain],
ownerAddress: params.ownerAddress,
@@ -32,6 +36,31 @@ function getTokenBalancesQuery(params: {
include_native: "true",
},
});
const result = await tryCatch(ownedTokenBalancePromise);
// fallback to fetch native token balance with rpc
if (result.error) {
const walletBalance = await getWalletBalance({
address: params.ownerAddress,
client: params.client,
chain: params.chain,
});
const nativeTokenBalance: GetBalanceResult = {
name: walletBalance.name,
value: walletBalance.value,
decimals: walletBalance.decimals,
displayValue: walletBalance.displayValue,
symbol: walletBalance.symbol,
chainId: params.chain.id,
tokenAddress: NATIVE_TOKEN_ADDRESS,
};
return [nativeTokenBalance];
}
return result.data;
},
queryKey: ["getOwnedTokens", params.chain.id, params.ownerAddress],
retry: false,
@@ -27,13 +27,15 @@ export const ClaimerSelection = () => {
setOpenSnapshotIndex: setOpenIndex,
isAdmin,
claimConditionType,
phaseSnapshots,
setPhaseSnapshot,
} = useClaimConditionsFormContext();
const handleClaimerChange = (value: string) => {
const val = value as "any" | "specific" | "overrides";
if (val === "any") {
form.setValue(`phases.${phaseIndex}.snapshot`, undefined);
setPhaseSnapshot(phaseIndex, undefined);
} else {
if (val === "specific") {
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 0);
@@ -41,7 +43,7 @@ export const ClaimerSelection = () => {
if (val === "overrides" && field.maxClaimablePerWallet !== 1) {
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 1);
}
form.setValue(`phases.${phaseIndex}.snapshot`, []);
setPhaseSnapshot(phaseIndex, []);
setOpenIndex(phaseIndex);
}
};
@@ -49,6 +51,7 @@ export const ClaimerSelection = () => {
let helperText: React.ReactNode;
const disabledSnapshotButton = isAdmin && formDisabled;
const snapshot = phaseSnapshots[phaseIndex];
if (dropType === "specific") {
helperText = (
@@ -87,10 +90,7 @@ export const ClaimerSelection = () => {
return (
<FormFieldSetup
errorMessage={
form.getFieldState(`phases.${phaseIndex}.snapshot`, form.formState)
?.error?.message
}
errorMessage={undefined}
helperText={helperText}
label={label}
isRequired={false}
@@ -117,7 +117,7 @@ export const ClaimerSelection = () => {
)}
{/* Edit or See Snapshot */}
{field.snapshot ? (
{snapshot ? (
<div className="flex items-center gap-3">
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */}
<Button
@@ -133,7 +133,7 @@ export const ClaimerSelection = () => {
<div
className={cn(
"flex gap-2 items-center",
field.snapshot?.length === 0
snapshot?.length === 0
? "text-muted-foreground"
: "text-green-600 dark:text-green-500",
disabledSnapshotButton ? "opacity-50" : "",
@@ -141,9 +141,8 @@ export const ClaimerSelection = () => {
>
<div className="size-2 bg-current rounded-full" />
<span className="text-sm">
{field.snapshot?.length}{" "}
{field.snapshot?.length === 1 ? "address" : "addresses"} in
snapshot
{snapshot?.length}{" "}
{snapshot?.length === 1 ? "address" : "addresses"} in snapshot
</span>
</div>
</div>
@@ -6,7 +6,15 @@ import {
CircleHelpIcon,
PlusIcon,
} from "lucide-react";
import { createContext, Fragment, useContext, useMemo, useState } from "react";
import {
createContext,
Fragment,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
type UseFieldArrayReturn,
type UseFormReturn,
@@ -120,21 +128,19 @@ export const ClaimConditionTypeData: Record<
});
const getClaimConditionTypeFromPhase = (
phase: ClaimConditionInput,
phase: Omit<ClaimConditionInput, "snapshot">,
snapshot: SnapshotEntry[] | undefined,
): ClaimConditionType => {
if (!phase.snapshot) {
if (!snapshot) {
return "public";
}
if (phase.snapshot) {
if (snapshot) {
if (
phase.maxClaimablePerWallet?.toString() === "0" &&
phase.price === "0" &&
typeof phase.snapshot !== "string" &&
phase.snapshot.length === 1 &&
phase.snapshot.some(
(a) => (a as SnapshotEntry).maxClaimable === "unlimited",
)
snapshot.length === 1 &&
snapshot.some((a) => a.maxClaimable === "unlimited")
) {
return "creator";
}
@@ -165,6 +171,11 @@ interface ClaimsConditionFormContextData {
isColumn?: boolean;
isErc20: boolean;
claimConditionType: ClaimConditionType;
phaseSnapshots: Record<number, SnapshotEntry[] | undefined>;
setPhaseSnapshot: (
phaseIndex: number,
snapshot: SnapshotEntry[] | undefined,
) => void;
}
// legacy, but we SHOULD remove this and instead pass down props!
@@ -207,6 +218,10 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
const isAdmin = useIsAdmin(contract);
const [openSnapshotIndex, setOpenSnapshotIndex] = useState(-1);
const [phaseSnapshots, setPhaseSnapshots] = useState<
Record<number, SnapshotEntry[] | undefined>
>({});
const sendTx = useSendAndConfirmTransaction();
const tokenDecimals = useReadContract(decimals, {
@@ -233,35 +248,56 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
}),
});
const isSnapshotInitialized = useRef(false);
// save snapshots in a separate state instead of form state because it makes rendering very slow for large snapshots (10k, 100k rows etc)
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (isSnapshotInitialized.current || !claimConditionsQuery.data) {
return;
}
const newPhaseSnapshots: Record<number, SnapshotEntry[] | undefined> = {};
claimConditionsQuery.data.forEach((phase, idx) => {
const snapshot = phase.snapshot?.map(
({ address, maxClaimable, price, currencyAddress }) => ({
address,
currencyAddress: currencyAddress || undefined,
maxClaimable: maxClaimable || "0",
price: price || undefined,
}),
);
newPhaseSnapshots[idx] = snapshot;
});
setPhaseSnapshots(newPhaseSnapshots);
isSnapshotInitialized.current = true;
}, [claimConditionsQuery.data]);
const transformedQueryData = useMemo(() => {
return (claimConditionsQuery.data || [])
.map((phase, idx) => ({
...phase,
currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
currencyMetadata: {
...phase.currencyMetadata,
value: phase.currencyMetadata.value?.toString() || "0",
},
fromSdk: true,
isEditing: false,
maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0",
maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0",
merkleRootHash: (phase.merkleRootHash || "") as string,
metadata: {
...phase.metadata,
name: phase?.metadata?.name || `Phase ${idx + 1}`,
},
price: phase.currencyMetadata.displayValue,
snapshot: phase.snapshot?.map(
({ address, maxClaimable, price, currencyAddress }) => ({
address,
currencyAddress: currencyAddress || undefined,
maxClaimable: maxClaimable || "0",
price: price || undefined,
}),
),
startTime: new Date(phase.startTime),
}))
.map((phase, idx) => {
return {
...phase,
currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
currencyMetadata: {
...phase.currencyMetadata,
value: phase.currencyMetadata.value?.toString() || "0",
},
fromSdk: true,
isEditing: false,
maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0",
maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0",
merkleRootHash: (phase.merkleRootHash || "") as string,
metadata: {
...phase.metadata,
name: phase?.metadata?.name || `Phase ${idx + 1}`,
},
price: phase.currencyMetadata.displayValue,
snapshot: undefined, // Do not save snapshot in form data - its slow
startTime: new Date(phase.startTime),
};
})
.filter(
(phase) => isMultiPhase || Number(phase.maxClaimableSupply) !== 0,
);
@@ -289,8 +325,19 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
name: "phases",
});
const setPhaseSnapshot = (
phaseIndex: number,
snapshot: SnapshotEntry[] | undefined,
) => {
setPhaseSnapshots((prev) => ({
...prev,
[phaseIndex]: snapshot,
}));
};
const addPhase = (type: ClaimConditionType) => {
const name = `${ClaimConditionTypeData[type].name} phase`;
const newPhaseIndex = formFields.fields.length;
switch (type) {
case "public":
@@ -305,8 +352,9 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
...DEFAULT_PHASE,
maxClaimablePerWallet: "0",
metadata: { name },
snapshot: [],
snapshot: undefined,
});
setPhaseSnapshot(newPhaseIndex, []);
break;
case "overrides":
@@ -314,8 +362,9 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
...DEFAULT_PHASE,
maxClaimablePerWallet: "1",
metadata: { name },
snapshot: [],
snapshot: undefined,
});
setPhaseSnapshot(newPhaseIndex, []);
break;
case "creator":
@@ -325,16 +374,17 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
maxClaimableSupply: "unlimited",
metadata: { name },
price: "0",
snapshot: walletAddress
? [
{
address: walletAddress,
maxClaimable: "unlimited",
price: "0",
},
]
: [],
snapshot: undefined,
});
if (walletAddress) {
setPhaseSnapshot(newPhaseIndex, [
{
address: walletAddress,
maxClaimable: "unlimited",
price: "0",
},
]);
}
break;
default:
@@ -360,6 +410,12 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
);
}
try {
// Merge snapshots back into phases for submission
const phasesWithSnapshots = d.phases.map((phase, index) => ({
...phase,
snapshot: phaseSnapshots[index] || undefined,
}));
const tx = setClaimPhasesTx(
{
contract,
@@ -370,19 +426,11 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
? { type: "erc721" }
: { tokenId: BigInt(tokenId || 0), type: "erc1155" }),
},
d.phases,
phasesWithSnapshots,
);
await sendTx.mutateAsync(tx);
saveClaimPhaseNotification.onSuccess();
const newPhases = d.phases.map((phase) => ({
...phase,
fromSdk: true,
isEditing: false,
}));
form.setValue("phases", newPhases);
} catch (error) {
console.error(error);
@@ -446,17 +494,21 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
{!isAdmin && <p>Connect with admin wallet to edit claim conditions.</p>}
{controlledFields.map((field, index) => {
const dropType: DropType = field.snapshot
const snapshot = phaseSnapshots[index];
const dropType: DropType = snapshot
? field.maxClaimablePerWallet?.toString() === "0"
? "specific"
: "overrides"
: "any";
const claimConditionType = getClaimConditionTypeFromPhase(field);
const claimConditionType = getClaimConditionTypeFromPhase(
field,
snapshot,
);
const isActive = activePhaseId === field.id;
const snapshotValue = field.snapshot?.map((v) =>
const snapshotValue = snapshot?.map((v) =>
typeof v === "string"
? {
address: v,
@@ -482,9 +534,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
onClose={() => {
setOpenSnapshotIndex(-1);
}}
setSnapshot={(snapshot) =>
form.setValue(`phases.${index}.snapshot`, snapshot)
}
setSnapshot={(snapshot) => setPhaseSnapshot(index, snapshot)}
value={snapshotValue}
/>
@@ -503,6 +553,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
phaseIndex: index,
setOpenSnapshotIndex,
tokenDecimals: tokenDecimals.data,
phaseSnapshots,
setPhaseSnapshot,
}}
>
<ClaimConditionsPhase
@@ -510,6 +562,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
isPending={sendTx.isPending}
onRemove={() => {
formFields.remove(index);
// Clean up snapshot when phase is removed
setPhaseSnapshot(index, undefined);
}}
/>
</ClaimsConditionFormContext.Provider>
@@ -35,12 +35,15 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
isActive,
isMultiPhase,
phaseIndex,
phaseSnapshots,
} = useClaimConditionsFormContext();
const toggleEditing = () => {
form.setValue(`phases.${phaseIndex}.isEditing`, !field.isEditing);
};
const snapshot = phaseSnapshots[phaseIndex];
return (
<DynamicHeight transition="height 0.3s ease">
<div className="border border-border rounded-lg bg-card relative">
@@ -118,9 +121,7 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
<PhaseStartTimeInput />
<CreatorInput
creatorAddress={
(field.snapshot?.[0] as { address: string })?.address
}
creatorAddress={(snapshot?.[0] as { address: string })?.address}
/>
<MaxClaimableSupplyInput />
@@ -4,9 +4,11 @@ import {
CircleSlashIcon,
RotateCcwIcon,
} from "lucide-react";
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
import { useState } from "react";
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
import { DownloadableCode } from "@/components/blocks/code/downloadable-code";
import { DropZone } from "@/components/blocks/drop-zone/drop-zone";
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
import { Button } from "@/components/ui/button";
import { InlineCode } from "@/components/ui/inline-code";
import { Spinner } from "@/components/ui/Spinner/Spinner";
@@ -27,24 +29,18 @@ import {
} from "@/components/ui/table";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useCsvUpload } from "@/hooks/useCsvUpload";
import type { SnapshotEntry } from "./legacy-zod-schema";
interface SnapshotAddressInput {
address: string;
maxClaimable?: string;
price?: string;
currencyAddress?: string;
isValid?: boolean;
}
interface SnapshotUploadProps {
setSnapshot: (snapshot: SnapshotAddressInput[]) => void;
setSnapshot: (snapshot: (SnapshotEntry & { ensName?: string })[]) => void;
dropType: "specific" | "any" | "overrides";
isDisabled: boolean;
value?: SnapshotAddressInput[] | undefined;
value?: (SnapshotEntry & { ensName?: string })[] | undefined;
onClose: () => void;
client: ThirdwebClient;
}
const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => {
const csvParser = (items: SnapshotEntry[]): SnapshotEntry[] => {
return items
.map(({ address, maxClaimable, price, currencyAddress }) => ({
address: (address || "").trim(),
@@ -55,92 +51,125 @@ const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => {
.filter(({ address }) => address !== "");
};
function SnapshotDataTable({ data }: { data: SnapshotAddressInput[] }) {
function SnapshotDataTable({
data,
}: {
data: (SnapshotEntry & {
ensName?: string;
})[];
}) {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const totalPages = Math.ceil(data.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentData = data.slice(startIndex, endIndex);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return (
<TableContainer>
<Table>
<TableHeader>
<TableRow>
<TableHead>Address</TableHead>
<TableHead>Max claimable</TableHead>
<TableHead>Price</TableHead>
<TableHead>Currency Address</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item) => (
<TableRow key={item.address}>
<TableCell>
{item.isValid ? (
item.address
) : (
<ToolTipLabel
label={
item.address === ZERO_ADDRESS
? "Cannot send tokens to ZERO_ADDRESS"
: item.address.startsWith("0x")
? "Address is not valid"
: "Address couldn't be resolved"
}
>
<div className="flex flex-row items-center gap-2">
<CircleAlertIcon className="size-4 text-red-500" />
<div className="cursor-default font-bold text-red-500">
{item.address}
</div>
</div>
</ToolTipLabel>
)}
</TableCell>
<TableCell>
{item.maxClaimable === "0" || !item.maxClaimable
? "Default"
: item.maxClaimable === "unlimited"
? "Unlimited"
: item.maxClaimable}
</TableCell>
<TableCell>
{item.price === "0"
? "Free"
: !item.price || item.price === "unlimited"
? "Default"
: item.price}
</TableCell>
<TableCell>
{item.currencyAddress ===
"0x0000000000000000000000000000000000000000" ||
!item.currencyAddress
? "Default"
: item.currencyAddress}
</TableCell>
<div className="border bg-card rounded-lg overflow-hidden">
<TableContainer className="border-none rounded-none">
<Table>
<TableHeader>
<TableRow>
<TableHead>Address</TableHead>
<TableHead>Max claimable</TableHead>
<TableHead>Price</TableHead>
<TableHead>Currency Address</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</TableHeader>
<TableBody>
{currentData.map((item) => (
<TableRow key={item.address}>
<TableCell>
{isAddress(item.address) && item.address !== ZERO_ADDRESS ? (
item.ensName || item.address
) : (
<ToolTipLabel
label={
item.address === ZERO_ADDRESS
? "Cannot send tokens to ZERO_ADDRESS"
: item.address.startsWith("0x")
? "Address is not valid"
: "Address couldn't be resolved"
}
>
<div className="flex flex-row items-center gap-2">
<CircleAlertIcon className="size-4 text-red-500" />
<div className="cursor-default font-bold text-red-500">
{item.ensName || item.address}
</div>
</div>
</ToolTipLabel>
)}
</TableCell>
<TableCell>
{item.maxClaimable === "0" || !item.maxClaimable
? "Default"
: item.maxClaimable === "unlimited"
? "Unlimited"
: item.maxClaimable}
</TableCell>
<TableCell>
{item.price === "0"
? "Free"
: !item.price || item.price === "unlimited"
? "Default"
: item.price}
</TableCell>
<TableCell>
{item.currencyAddress ===
"0x0000000000000000000000000000000000000000" ||
!item.currencyAddress
? "Default"
: item.currencyAddress}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{totalPages > 1 && (
<div className="border-t p-4 lg:px-6">
<PaginationButtons
activePage={currentPage}
totalPages={totalPages}
onPageClick={handlePageChange}
/>
</div>
)}
</div>
);
}
const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
const UploadSnapshot: React.FC<Omit<SnapshotUploadProps, "value">> = ({
setSnapshot,
dropType,
isDisabled,
value,
onClose,
client,
}) => {
const csvUpload = useCsvUpload<SnapshotAddressInput>({
const csvUpload = useCsvUpload<SnapshotEntry>({
client,
csvParser,
defaultRawData: value,
});
const normalizeData = csvUpload.normalizeQuery.data;
if (!normalizeData) {
return (
<div className="flex min-h-[400px] w-full grow items-center justify-center">
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center">
<Spinner className="size-10" />
<p className="text-base text-foreground mt-5">Resolving ENS</p>
<p className="text-sm text-muted-foreground mt-2">
{csvUpload.normalizeProgress.current} /{" "}
{csvUpload.normalizeProgress.total}
</p>
</div>
);
}
@@ -154,6 +183,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
currencyAddress: o.currencyAddress,
maxClaimable: o.maxClaimable,
price: o.price,
ensName: o.address.startsWith("0x") ? undefined : o.address,
})),
);
onClose();
@@ -163,41 +193,58 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
<div className="space-y-6">
{csvUpload.rawData.length > 0 ? (
<div>
<SnapshotDataTable data={csvUpload.normalizeQuery.data.result} />
<div className="flex justify-end gap-3 border-t py-6 border-dashed">
<Button
className="gap-2"
disabled={isDisabled || csvUpload.rawData.length === 0}
onClick={() => {
csvUpload.reset();
}}
variant="outline"
>
<RotateCcwIcon className="size-4" />
Reset
</Button>
<SnapshotDataTable
data={csvUpload.normalizeQuery.data.result.map((x) => ({
address: x.resolvedAddress,
ensName: x.address.startsWith("0x") ? undefined : x.address,
maxClaimable: x.maxClaimable,
currencyAddress: x.currencyAddress,
price: x.price,
}))}
/>
{csvUpload.normalizeQuery.data?.invalidFound ? (
<div className="py-6">
{csvUpload.normalizeQuery.data.invalidFound && (
<p className="text-sm text-destructive-text mb-3">
Invalid addresses found, please remove from the list to continue
</p>
)}
<div className="flex justify-start items-center gap-3">
<Button
className="gap-2"
disabled={isDisabled || csvUpload.rawData.length === 0}
onClick={() => {
csvUpload.removeInvalid();
csvUpload.reset();
}}
variant="outline"
>
<CircleSlashIcon className="size-4" />
Remove invalid
<RotateCcwIcon className="size-4" />
Reset
</Button>
) : (
<Button
className="gap-2"
disabled={isDisabled || csvUpload.rawData.length === 0}
onClick={onSave}
>
Next
<ArrowRightIcon className="size-4" />
</Button>
)}
{csvUpload.normalizeQuery.data?.invalidFound ? (
<Button
className="gap-2"
disabled={isDisabled || csvUpload.rawData.length === 0}
onClick={() => {
csvUpload.removeInvalid();
}}
>
<CircleSlashIcon className="size-4" />
Remove invalid
</Button>
) : (
<Button
className="gap-2"
disabled={isDisabled || csvUpload.rawData.length === 0}
onClick={onSave}
>
Next
<ArrowRightIcon className="size-4" />
</Button>
)}
</div>
</div>
</div>
) : (
@@ -308,6 +355,23 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
);
};
function ViewSnapshot(props: {
data: (SnapshotEntry & { ensName?: string })[];
onReset: () => void;
}) {
return (
<div>
<SnapshotDataTable data={props.data} />
<div className="py-6">
<Button className="gap-2" onClick={props.onReset} variant="outline">
<RotateCcwIcon className="size-4" />
Reset
</Button>
</div>
</div>
);
}
export function SnapshotViewerSheet(
props: SnapshotUploadProps & {
isOpen: boolean;
@@ -322,7 +386,7 @@ export function SnapshotViewerSheet(
}}
open={props.isOpen}
>
<SheetContent className="!w-full !max-w-4xl overflow-y-auto">
<SheetContent className="!w-full !max-w-4xl overflow-y-auto flex flex-col">
<SheetHeader className="mb-3">
<SheetTitle className="text-left">Snapshot</SheetTitle>
</SheetHeader>
@@ -332,6 +396,33 @@ export function SnapshotViewerSheet(
);
}
function SnapshotViewerSheetContent(props: SnapshotUploadProps) {
const [showEditMode, setShowEditMode] = useState(false);
if (showEditMode) {
return (
<UploadSnapshot
setSnapshot={props.setSnapshot}
dropType={props.dropType}
isDisabled={props.isDisabled}
onClose={props.onClose}
client={props.client}
/>
);
}
if (props.value && props.value.length > 0) {
return (
<ViewSnapshot
data={props.value || []}
onReset={() => setShowEditMode(true)}
/>
);
}
return <UploadSnapshot {...props} />;
}
const snapshotWithMaxClaimable = `\
address,maxClaimable
0x0000000000000000000000000000000000000000,2
@@ -29,7 +29,7 @@ export function ContractSplitPage({
}) {
const address = useActiveAccount()?.address;
const { data: allRecipientsPercentages } = useReadContract(
const allRecipientsPercentagesQuery = useReadContract(
getAllRecipientsPercentages,
{ contract },
);
@@ -39,156 +39,163 @@ export function ContractSplitPage({
chain: contract.chain,
});
const activeRecipient = (allRecipientsPercentages || []).find(
const activeRecipient = (allRecipientsPercentagesQuery.data || []).find(
(r) => r.address.toLowerCase() === address?.toLowerCase(),
);
return (
<div>
{/* header */}
<div className="flex flex-col gap-3 lg:flex-row lg:items-center justify-between mb-6 lg:mb-4">
<div>
<h2 className="text-2xl font-semibold tracking-tight mb-1">
Balances
</h2>
<p className="text-muted-foreground">
The Split can receive funds in the native token or in any ERC20
</p>
</div>
<div className="mb-4">
<h2 className="text-2xl font-semibold tracking-tight">Balances</h2>
<p className="text-muted-foreground">
The Split can receive funds in the native token or in any ERC20
</p>
</div>
{/* balances table */}
<div className="mb-10">
<div className="border rounded-lg overflow-hidden">
<TableContainer className="border-none">
<Table>
<TableHeader>
<TableRow>
<TableHead>Token</TableHead>
<TableHead>Balance</TableHead>
{activeRecipient && <TableHead>Your Share</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{balanceQuery.isPending
? new Array(3).fill(null).map((_, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: ok
<TableRow key={index}>
<TableCell>
<Skeleton className="h-5 w-52" />
</TableCell>
<TableCell>
<Skeleton className="h-5 w-16" />
</TableCell>
{activeRecipient && (
<TableCell>
<Skeleton className="h-5 w-16" />
</TableCell>
)}
</TableRow>
))
: balanceQuery.data?.map((tokenBalance) => (
<TableRow key={tokenBalance.tokenAddress}>
{/* token */}
<TableCell className="font-medium">
<Button
asChild
variant="ghost"
className="flex items-center gap-2 w-fit h-auto py-1 px-2 -translate-x-2"
>
{tokenBalance.tokenAddress ===
NATIVE_TOKEN_ADDRESS ? (
<span>{tokenBalance.symbol}</span>
) : (
<Link
href={`https://thirdweb.com/${tokenBalance.chainId}/${tokenBalance.tokenAddress}`}
target="_blank"
>
{tokenBalance.name}
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
</Link>
)}
</Button>
</TableCell>
{/* balance */}
<TableCell>
{tokenBalance.displayValue} {tokenBalance.symbol}
</TableCell>
{/* your share percent */}
{activeRecipient && (
<TableCell>
{activeRecipient.splitPercentage}%
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
{balanceQuery.isError && (
<div className="text-red-500 px-4 flex justify-center items-center py-20 text-muted-foreground">
{balanceQuery.error.message}
</div>
)}
{!balanceQuery.isPending && balanceQuery.data?.length === 0 && (
<div className="px-4 flex justify-center items-center py-20 text-muted-foreground">
No funds received yet
</div>
)}
</TableContainer>
{balanceQuery.data && balanceQuery.data.length > 0 && (
<div className="border-t p-4 lg:px-6 flex justify-end bg-card">
<DistributeButton
balances={balanceQuery.data || []}
balancesIsError={balanceQuery.isError}
balancesIsPending={balanceQuery.isPending}
contract={contract}
isLoggedIn={isLoggedIn}
/>
</div>
)}
</div>
</div>
{/* recipients table */}
<div>
<div className="mb-4">
<h3 className="text-2xl font-semibold tracking-tight mb-1">
Split Recipients
</h3>
<p className="text-muted-foreground">
List of addresses that can receive funds from the Split and their
percentage share.
</p>
</div>
<TableContainer>
<div className="bg-card border rounded-lg overflow-hidden">
<TableContainer className="border-none rounded-none">
<Table>
<TableHeader>
<TableRow>
<TableHead>Address</TableHead>
<TableHead>Percentage</TableHead>
<TableHead className="w-[240px] lg:w-[320px]">Token</TableHead>
<TableHead>Balance</TableHead>
{activeRecipient && <TableHead>Your Share</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{(allRecipientsPercentages || []).map((split) => (
<TableRow key={split.address}>
<TableCell>
<WalletAddress
address={split.address}
client={contract.client}
/>
</TableCell>
<TableCell>{split.splitPercentage}%</TableCell>
</TableRow>
))}
{balanceQuery.isPending
? new Array(3).fill(null).map((_, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: ok
<TableRow key={index}>
<TableCell>
<Skeleton className="h-5 w-52" />
</TableCell>
<TableCell>
<Skeleton className="h-5 w-16" />
</TableCell>
{activeRecipient && (
<TableCell>
<Skeleton className="h-5 w-16" />
</TableCell>
)}
</TableRow>
))
: balanceQuery.data?.map((tokenBalance) => (
<TableRow key={tokenBalance.tokenAddress}>
{/* token */}
<TableCell className="font-medium">
<Button
asChild
variant="ghost"
className="flex items-center gap-2 w-fit h-auto py-1 px-2 -translate-x-2"
>
{tokenBalance.tokenAddress ===
NATIVE_TOKEN_ADDRESS ? (
<span>{tokenBalance.symbol}</span>
) : (
<Link
href={`https://thirdweb.com/${tokenBalance.chainId}/${tokenBalance.tokenAddress}`}
target="_blank"
>
{tokenBalance.name}
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
</Link>
)}
</Button>
</TableCell>
{/* balance */}
<TableCell>
{tokenBalance.displayValue} {tokenBalance.symbol}
</TableCell>
{/* your share percent */}
{activeRecipient && (
<TableCell>
{activeRecipient.splitPercentage}%
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
{balanceQuery.isError && (
<div className="text-red-500 px-4 flex justify-center items-center py-20 text-muted-foreground">
{balanceQuery.error.message}
</div>
)}
{!balanceQuery.isPending && balanceQuery.data?.length === 0 && (
<div className="px-4 flex justify-center items-center py-20 text-muted-foreground">
No funds received yet
</div>
)}
</TableContainer>
{balanceQuery.data && balanceQuery.data.length > 0 && (
<div className="border-t p-4 lg:py-5 lg:px-6 flex justify-end bg-card">
<DistributeButton
balances={balanceQuery.data || []}
balancesIsError={balanceQuery.isError}
balancesIsPending={balanceQuery.isPending}
contract={contract}
isLoggedIn={isLoggedIn}
/>
</div>
)}
</div>
<div className="h-10" />
<div className="mb-4">
<h3 className="text-2xl font-semibold tracking-tight">
Split Recipients
</h3>
<p className="text-muted-foreground">
List of addresses that can receive funds from the Split and their
percentage share.
</p>
</div>
{/* recipients table */}
<TableContainer>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[240px] lg:w-[320px]">Address</TableHead>
<TableHead>Percentage</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allRecipientsPercentagesQuery.isPending
? new Array(3).fill(null).map((_, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: ok
<TableRow key={index}>
<TableCell>
<Skeleton className="h-5 w-52" />
</TableCell>
<TableCell>
<Skeleton className="h-5 w-16" />
</TableCell>
</TableRow>
))
: (allRecipientsPercentagesQuery.data || []).map((split) => (
<TableRow key={split.address}>
<TableCell>
<WalletAddress
address={split.address}
client={contract.client}
className="h-auto py-1"
/>
</TableCell>
<TableCell>{split.splitPercentage}%</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
);
}
@@ -61,6 +61,7 @@ export const DistributeButton = ({
isLoggedIn={isLoggedIn}
isPending={mutation.isPending}
onClick={distributeFunds}
size="sm"
transactionCount={undefined}
// if we fail to get the balances, we can't know how many transactions there are going to be
txChainID={contract.chain.id}
@@ -87,7 +88,7 @@ export const DistributeButton = ({
isLoggedIn={isLoggedIn}
isPending={mutation.isPending}
onClick={distributeFunds}
transactionCount={numTransactions}
transactionCount={numTransactions === 1 ? undefined : numTransactions}
txChainID={contract.chain.id}
variant="default"
size="sm"
@@ -21,12 +21,29 @@ export function AirdropUpload(props: {
client: props.client,
csvParser,
});
if (csvUpload.normalizeQuery.isPending) {
return (
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center">
<Spinner className="size-10" />
<p className="text-base text-foreground mt-5">Resolving ENS</p>
<p className="text-sm text-muted-foreground mt-2">
{csvUpload.normalizeProgress.current} /{" "}
{csvUpload.normalizeProgress.total}
</p>
</div>
);
}
const normalizeData = csvUpload.normalizeQuery.data;
if (!normalizeData) {
return (
<div className="flex min-h-[400px] w-full grow items-center justify-center rounded-lg border border-border">
<Spinner className="size-10" />
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center text-center">
<p className="text-base text-foreground">Failed to resolve ENS</p>
<p className="text-sm text-muted-foreground mt-2">
{String(csvUpload.normalizeQuery.error)}
</p>
</div>
);
}
@@ -262,8 +262,13 @@ const AirdropUpload: React.FC<AirdropUploadProps> = ({
if (csvUpload.normalizeQuery.isPending) {
return (
<div className="flex h-[300px] w-full grow items-center justify-center rounded-lg border border-border">
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center">
<Spinner className="size-10" />
<p className="text-base text-foreground mt-5">Resolving ENS</p>
<p className="text-sm text-muted-foreground mt-2">
{csvUpload.normalizeProgress.current} /{" "}
{csvUpload.normalizeProgress.total}
</p>
</div>
);
}
@@ -3,7 +3,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import type { ProjectEmbeddedWalletsService } from "@thirdweb-dev/service-utils";
import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
import Link from "next/link";
import type React from "react";
import { useState } from "react";
import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
@@ -240,11 +239,7 @@ export const InAppWalletSettingsUI: React.FC<
return (
<Form {...form}>
<form
autoComplete="off"
className="flex flex-col gap-6"
onSubmit={handleSubmit}
>
<form autoComplete="off" className="space-y-6" onSubmit={handleSubmit}>
{/* Branding */}
<BrandingFieldset
client={props.client}
@@ -252,12 +247,28 @@ export const InAppWalletSettingsUI: React.FC<
requiredPlan={brandingRequiredPlan}
teamPlan={props.teamPlan}
teamSlug={props.teamSlug}
isUpdating={props.isUpdating}
/>
<NativeAppsFieldset form={form} />
<NativeAppsFieldset form={form} isUpdating={props.isUpdating} />
{/* Authentication */}
<Fieldset legend="Authentication">
<Fieldset
legend="Authentication"
footer={
<div className="flex justify-end p-4 md:px-6 border-t border-dashed">
<Button
className="gap-2"
type="submit"
size="sm"
variant="outline"
>
{props.isUpdating && <Spinner className="size-4" />}
Save
</Button>
</div>
}
>
<JSONWebTokenFields
form={form}
requiredPlan={authRequiredPlan}
@@ -265,7 +276,7 @@ export const InAppWalletSettingsUI: React.FC<
teamSlug={props.teamSlug}
/>
<div className="h-5" />
<div className="my-5 border-t border-dashed" />
<AuthEndpointFields
form={form}
@@ -274,7 +285,7 @@ export const InAppWalletSettingsUI: React.FC<
teamSlug={props.teamSlug}
/>
<div className="h-5" />
<div className="my-5 border-t border-dashed" />
<SMSCountryFields
form={form}
@@ -284,13 +295,6 @@ export const InAppWalletSettingsUI: React.FC<
teamSlug={props.teamSlug}
/>
</Fieldset>
<div className="flex justify-end">
<Button className="gap-2" type="submit" variant="primary">
{props.isUpdating && <Spinner className="size-4" />}
Save changes
</Button>
</div>
</form>
</Form>
);
@@ -302,14 +306,22 @@ function BrandingFieldset(props: {
teamSlug: string;
requiredPlan: Team["billingPlan"];
client: ThirdwebClient;
isUpdating: boolean;
}) {
return (
<Fieldset legend="Branding">
<SwitchContainer
description="Pass a custom logo and app name to be used in the emails sent to users."
switchId="branding-switch"
title="Custom email logo and name"
>
<FieldsetWithDescription
legend="Branding"
description="Set an app name and logo to be used in the emails sent to users"
footer={
<div className="flex justify-end p-4 md:px-6 border-t border-dashed">
<Button className="gap-2" type="submit" size="sm" variant="outline">
{props.isUpdating && <Spinner className="size-4" />}
Save
</Button>
</div>
}
>
<div className="absolute top-8 right-6">
<GatedSwitch
currentPlan={props.teamPlan}
requiredPlan={props.requiredPlan}
@@ -330,64 +342,69 @@ function BrandingFieldset(props: {
teamSlug={props.teamSlug}
trackingLabel="customEmailLogoAndName"
/>
</SwitchContainer>
</div>
<GatedCollapsibleContainer
className="grid grid-cols-1 gap-6 lg:grid-cols-2"
className="border-t border-dashed pt-5"
currentPlan={props.teamPlan}
isExpanded={!!props.form.watch("branding")}
requiredPlan={props.requiredPlan}
>
{/* Application Image */}
<FormField
control={props.form.control}
name="branding.applicationImageUrl"
render={() => (
<FormItem className="space-y-1">
<FormLabel>Application Image URL</FormLabel>
<FormDescription className="!mb-4">
Logo that will display in the emails sent to users.{" "}
<br className="max-sm:hidden" /> The image must be squared with
recommended size of 72x72 px.
</FormDescription>
<FormControl>
<AppImageFormControl
client={props.client}
setUri={(uri) => {
props.form.setValue("branding.applicationImageUrl", uri, {
shouldDirty: true,
shouldTouch: true,
});
}}
uri={props.form.watch("branding.applicationImageUrl")}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-col gap-6 lg:flex-row">
<FormField
control={props.form.control}
name="branding.applicationImageUrl"
render={() => (
<FormItem className="lg:border-r lg:border-dashed lg:pr-6">
<div className="space-y-1">
<FormLabel>Application Image URL</FormLabel>
<FormDescription className="!mb-4">
Logo that will display in the emails sent to users.{" "}
<br className="max-sm:hidden" /> The image must be squared
with recommended size of 72x72 px.
</FormDescription>
{/* Application Name */}
<FormField
control={props.form.control}
name="branding.applicationName"
render={({ field }) => (
<FormItem>
<FormLabel>Application Name</FormLabel>
<FormDescription className="!mb-2">
Name that will be displayed in the emails sent to users.{" "}
<br className="max-sm:hidden" /> Defaults to your API Key's
name.
</FormDescription>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</div>
<FormControl>
<AppImageFormControl
client={props.client}
setUri={(uri) => {
props.form.setValue("branding.applicationImageUrl", uri, {
shouldDirty: true,
shouldTouch: true,
});
}}
uri={props.form.watch("branding.applicationImageUrl")}
/>
</FormControl>
</FormItem>
)}
/>
{/* Application Name */}
<FormField
control={props.form.control}
name="branding.applicationName"
render={({ field }) => (
<FormItem>
<FormLabel>Application Name</FormLabel>
<FormDescription>
Name that will be displayed in the emails sent to users.{" "}
<br className="max-sm:hidden" /> Defaults to your API Key's
name.
</FormDescription>
<FormControl>
<Input {...field} className="max-w-md" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</GatedCollapsibleContainer>
</Fieldset>
</FieldsetWithDescription>
);
}
@@ -418,7 +435,7 @@ function AppImageFormControl(props: {
<div className="relative">
<FileInput
accept={{ "image/*": [] }}
className="w-24 rounded-full bg-background lg:w-28"
className="w-24 rounded-full bg-background"
client={props.client}
disableHelperText
setValue={async (v) => {
@@ -514,14 +531,13 @@ function JSONWebTokenFields(props: {
description={
<>
Optionally allow users to authenticate with a custom JWT.{" "}
<Link
className="text-link-foreground hover:text-foreground"
<UnderlineLink
href="https://portal.thirdweb.com/wallets/custom-auth"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</Link>
</UnderlineLink>
</>
}
switchId="authentication-switch"
@@ -606,14 +622,13 @@ function AuthEndpointFields(props: {
<>
Optionally allow users to authenticate with any arbitrary payload
that you provide.{" "}
<Link
className="text-link-foreground hover:text-foreground"
<UnderlineLink
href="https://portal.thirdweb.com/wallets/custom-auth"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</Link>
</UnderlineLink>
</>
}
switchId="auth-endpoint-switch"
@@ -743,10 +758,21 @@ function AuthEndpointFieldsContent(props: {
function NativeAppsFieldset(props: {
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
isUpdating: boolean;
}) {
const { form } = props;
return (
<Fieldset legend="Native Apps">
<Fieldset
legend="Native Apps"
footer={
<div className="flex justify-end p-4 md:px-6 border-t border-dashed">
<Button className="gap-2" type="submit" size="sm" variant="outline">
{props.isUpdating && <Spinner className="size-4" />}
Save
</Button>
</div>
}
>
<FormField
control={form.control}
name="redirectUrls"
@@ -789,21 +815,60 @@ function GatedCollapsibleContainer(props: {
return null;
}
return <div className={cn("mt-6", props.className)}>{props.children}</div>;
return (
<div
className={cn("mt-5 fade-in-0 duration-300 animate-in", props.className)}
>
{props.children}
</div>
);
}
function Fieldset(props: { legend: string; children: React.ReactNode }) {
function Fieldset(props: {
legend: string;
children: React.ReactNode;
footer?: React.ReactNode;
}) {
return (
<DynamicHeight>
<fieldset className="rounded-lg border border-border bg-card p-4 md:p-6">
{/* put inside div to remove default styles on legend */}
<div className="mb-4 font-semibold text-xl tracking-tight">
<legend> {props.legend}</legend>
</div>
<div className="rounded-lg border bg-card relative">
<DynamicHeight>
<fieldset className="p-4 md:px-6 md:py-5">
{/* put inside div to remove default styles on legend */}
<div className="border-b pb-4 mb-5 border-dashed font-semibold text-xl tracking-tight">
<legend> {props.legend}</legend>
</div>
{props.children}
</fieldset>
</DynamicHeight>
{props.children}
</fieldset>
</DynamicHeight>
{props.footer}
</div>
);
}
function FieldsetWithDescription(props: {
legend: string;
children: React.ReactNode;
footer?: React.ReactNode;
description: React.ReactNode;
}) {
return (
<div className="rounded-lg border bg-card relative">
<DynamicHeight>
<fieldset className="p-4 md:p-6">
{/* put inside div to remove default styles on legend */}
<div className="pr-20">
<legend className="font-semibold text-xl tracking-tight">
{props.legend}
</legend>
<p className="text-muted-foreground text-sm">{props.description}</p>
</div>
{props.children}
</fieldset>
</DynamicHeight>
{props.footer}
</div>
);
}
@@ -1,6 +1,8 @@
/** biome-ignore-all lint/a11y/useSemanticElements: EXPECTED */
import { CheckIcon, MinusIcon } from "lucide-react";
import { CheckIcon, ChevronDownIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import type { SMSCountryTiers } from "../../api/sms";
@@ -31,17 +33,6 @@ export default function CountrySelector({
return tierCountries.every((country) => isCountrySelected(country));
};
// Check if some countries in a tier are selected
const isTierIndeterminate = (tier: string) => {
const tierCountries = countryTiers[tier as keyof typeof countryTiers];
const selectedInTier = tierCountries.filter((country) =>
isCountrySelected(country),
);
return (
selectedInTier.length > 0 && selectedInTier.length < tierCountries.length
);
};
// Toggle a tier selection
const toggleTier = (tier: string) => {
const tierCountries = countryTiers[tier as keyof typeof countryTiers];
@@ -81,101 +72,144 @@ export default function CountrySelector({
onChange(newSelected);
};
// Get selected countries count for a tier
const getSelectedCountInTier = (tier: string) => {
const tierCountries = countryTiers[tier as keyof typeof countryTiers];
return tierCountries.filter((country) => isCountrySelected(country)).length;
};
return (
<div className="space-y-3">
{Object.entries(countryTiers).map(([tier, countries], index) => (
<div className="overflow-hidden rounded-md border" key={tier}>
<div className="flex items-center bg-muted/30 p-2">
<div className="relative mr-2 flex items-center">
<Checkbox
checked={isTierSelected(tier)}
className="h-4 w-4"
id={`tier-${index + 1}`}
onCheckedChange={() => toggleTier(tier)}
/>
{isTierIndeterminate(tier) && (
<MinusIcon className="-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute top-1/2 left-1/2 h-2.5 w-2.5 transform text-primary" />
)}
</div>
<label
className="flex-1 cursor-pointer font-medium text-sm"
htmlFor={`tier-${index + 1}`}
>
Tier {index + 1}
{isTierIndeterminate(tier) && (
<span className="ml-1 font-normal text-muted-foreground text-xs">
({getSelectedCountInTier(tier)}/{countries.length})
</span>
)}
</label>
<span
className={cn(
"font-medium text-xs",
tier === "tier1"
? "text-green-600 dark:text-green-500"
: "text-muted-foreground",
)}
>
{tierPricing[tier as keyof typeof tierPricing]}
</span>
</div>
<div className="space-y-5">
{Object.entries(countryTiers).map(([tier, tierCountries], index) => {
const selectedTierCountries = tierCountries.filter((country) =>
isCountrySelected(country),
);
<div
className={cn(
"grid grid-cols-2 gap-1.5 p-2",
countries.length === 2
? "md:grid-cols-2"
: countries.length === 3
? "md:grid-cols-3"
: "md:grid-cols-4",
)}
>
{countries.map((country) => (
<div
className={cn(
"relative flex cursor-pointer items-center justify-between rounded-md border p-1.5 transition-colors ",
isCountrySelected(country)
? "border-primary bg-primary/10"
: "border-transparent hover:bg-muted/50",
)}
key={country}
onClick={() => toggleCountry(country)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
toggleCountry(country);
}
}}
role="button"
tabIndex={0}
title={countryNames[country] || country}
>
<span className="flex items-center space-x-2">
<span aria-hidden="true" className="text-lg">
{getCountryFlag(country)}
</span>
<span className="font-medium text-xs">
{countryNames[country] || country}
{countryPrefixes[country]
? ` (${countryPrefixes[country]})`
: ""}
</span>
</span>
<div className="flex h-3 w-3 items-center justify-center">
{isCountrySelected(country) && (
<CheckIcon className="h-3 w-3 text-primary" />
)}
</div>
</div>
))}
</div>
</div>
))}
return (
<TierCard
key={tier}
tierIndex={index}
tier={tier}
onToggleTier={() => toggleTier(tier)}
tierCountries={tierCountries}
selectedTierCountries={selectedTierCountries}
onToggleCountry={toggleCountry}
/>
);
})}
</div>
);
}
function TierCard(props: {
tier: string;
tierIndex: number;
onToggleTier: () => void;
tierCountries: string[];
selectedTierCountries: string[];
onToggleCountry: (country: string) => void;
}) {
const {
tier,
tierIndex,
onToggleTier,
tierCountries: countries,
selectedTierCountries: selectedCountries,
onToggleCountry,
} = props;
const [isExpanded, setIsExpanded] = useState(true);
const isPartiallySelected =
selectedCountries.length > 0 && selectedCountries.length < countries.length;
const isTierFullySelected = selectedCountries.length === countries.length;
return (
<div className="overflow-hidden rounded-xl border bg-background">
{/* header */}
<div className="flex items-center px-4 py-3 gap-3 justify-between">
{/* left */}
<div className="flex items-center gap-2">
<Checkbox
checked={
isPartiallySelected ? "indeterminate" : isTierFullySelected
}
id={`tier-${tier}`}
onCheckedChange={onToggleTier}
/>
<label
className="flex-1 cursor-pointer font-medium text-sm"
htmlFor={`tier-${tier}`}
>
Tier {tierIndex + 1}
</label>
{isPartiallySelected && (
<span className="text-muted-foreground text-sm">
({selectedCountries.length}/{countries.length})
</span>
)}
</div>
{/* right */}
<div className="flex items-center gap-3">
<span
className={cn(
"text-sm",
tier === "tier1"
? "text-green-600 dark:text-green-500"
: "text-muted-foreground",
)}
>
{tierPricing[tier as keyof typeof tierPricing]}
</span>
<Button
variant="ghost"
className="p-0 size-7"
onClick={() => setIsExpanded(!isExpanded)}
>
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isExpanded && "rotate-180",
)}
/>
</Button>
</div>
</div>
{/* body */}
{isExpanded && (
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 translate-y-[1px] translate-x-[1px] -ml-[1px] border-t",
)}
>
{countries.map((country) => {
const isSelected = selectedCountries.includes(country);
return (
<Button
variant="outline"
className={cn(
"justify-between gap-3 text-start h-auto rounded-none border-l-0 border-t-0 border-border py-2.5 bg-background",
)}
key={country}
onClick={() => onToggleCountry(country)}
title={countryNames[country] || country}
>
<div className="flex items-center gap-3">
<div className="text-3xl">{getCountryFlag(country)}</div>
<div className="space-y-0.5">
<div className="font-medium text-sm">
{countryNames[country] || country}
</div>
<div className="text-muted-foreground text-xs tabular-nums">
{countryPrefixes[country]
? `${countryPrefixes[country]}`
: ""}
</div>
</div>
</div>
{isSelected && <CheckIcon className="size-5 text-foreground" />}
</Button>
);
})}
</div>
)}
</div>
);
}
@@ -117,14 +117,14 @@ export const countryNames: Record<string, string> = {
KG: "Kyrgyzstan",
KH: "Cambodia",
KI: "Kiribati",
KN: "St Kitts and Nevis",
KR: "Korea Republic of",
KN: "Saint Kitts and Nevis",
KR: "South Korea",
KW: "Kuwait",
KY: "Cayman Islands",
KZ: "Kazakhstan",
LA: "Laos PDR",
LB: "Lebanon",
LC: "St Lucia",
LC: "Saint Lucia",
LI: "Liechtenstein",
LK: "Sri Lanka",
LR: "Liberia",
@@ -139,7 +139,7 @@ export const countryNames: Record<string, string> = {
ME: "Montenegro",
MG: "Madagascar",
MH: "Marshall Islands",
MK: "Macedonia",
MK: "North Macedonia",
ML: "Mali",
MM: "Myanmar",
MN: "Mongolia",
@@ -172,14 +172,14 @@ export const countryNames: Record<string, string> = {
PH: "Philippines",
PK: "Pakistan",
PL: "Poland",
PM: "St Pierre and Miquelon",
PM: "Saint Pierre and Miquelon",
PR: "Puerto Rico",
PS: "Palestinian Territory",
PT: "Portugal",
PW: "Palau",
PY: "Paraguay",
QA: "Qatar",
RE: "Reunion/Mayotte",
RE: "Réunion",
RO: "Romania",
RS: "Serbia",
RW: "Rwanda",
@@ -200,7 +200,7 @@ export const countryNames: Record<string, string> = {
ST: "Sao Tome and Principe",
SV: "El Salvador",
SY: "Syria",
SZ: "Swaziland",
SZ: "Eswatini",
TC: "Turks and Caicos Islands",
TD: "Chad",
TG: "Togo",
@@ -217,7 +217,7 @@ export const countryNames: Record<string, string> = {
UG: "Uganda",
US: "United States",
UY: "Uruguay",
VC: "St Vincent Grenadines",
VC: "Saint Vincent and the Grenadines",
VE: "Venezuela",
VG: "Virgin Islands, British",
VI: "Virgin Islands, U.S.",
+5
View File
@@ -18,6 +18,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "4.1.0",
"fetch-event-stream": "0.1.5",
"lucide-react": "0.525.0",
"next": "15.3.5",
"next-themes": "^0.4.6",
@@ -26,11 +27,15 @@
"posthog-js": "1.256.1",
"prettier": "3.6.2",
"react": "19.1.0",
"react-children-utilities": "^2.10.0",
"react-dom": "19.1.0",
"react-hook-form": "7.55.0",
"react-markdown": "10.1.0",
"react-pick-color": "^2.0.0",
"remark-gfm": "4.0.1",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "2.0.6",
"tailwind-merge": "^2.6.0",
"thirdweb": "workspace:*",
"use-debounce": "^10.0.5",
+305
View File
@@ -0,0 +1,305 @@
import { stream } from "fetch-event-stream";
import type { NebulaTxData, NebulaUserMessage } from "./types";
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
export type NebulaContext = {
chainIds: string[] | null;
walletAddress: string | null;
sessionId: string | null;
};
type NebulaSwapData = {
action: string;
transaction: {
chainId: number;
to: `0x${string}`;
data: `0x${string}`;
};
to: {
address: `0x${string}`;
amount: string;
chain_id: number;
decimals: number;
symbol: string;
};
from: {
address: `0x${string}`;
amount: string;
chain_id: number;
decimals: number;
symbol: string;
};
intent: {
amount: string;
destinationChainId: number;
destinationTokenAddress: `0x${string}`;
originChainId: number;
originTokenAddress: `0x${string}`;
receiver: `0x${string}`;
sender: `0x${string}`;
};
};
export async function promptNebula(params: {
message: NebulaUserMessage;
handleStream: (res: ChatStreamedResponse) => void;
abortController: AbortController;
context: undefined | NebulaContext;
}) {
const body: Record<string, string | boolean | object> = {
messages: [params.message],
stream: true,
};
if (params.context) {
body.context = {
chain_ids: params.context.chainIds?.map(Number) || [],
session_id: params.context.sessionId ?? undefined,
wallet_address: params.context.walletAddress,
};
}
const events = await stream(`${API_URL}/ai/chat`, {
body: JSON.stringify(body),
headers: {
"x-client-id": process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!,
"Content-Type": "application/json",
},
method: "POST",
signal: params.abortController.signal,
});
for await (const _event of events) {
if (!_event.data) {
continue;
}
const event = _event as ChatStreamedEvent;
switch (event.event) {
case "delta": {
params.handleStream({
data: {
v: JSON.parse(event.data).v,
},
event: "delta",
});
break;
}
case "presence": {
params.handleStream({
data: JSON.parse(event.data),
event: "presence",
});
break;
}
case "image": {
const data = JSON.parse(event.data) as {
data: {
width: number;
height: number;
url: string;
};
request_id: string;
};
params.handleStream({
data: data.data,
event: "image",
request_id: data.request_id,
});
break;
}
case "action": {
const data = JSON.parse(event.data);
if (data.type === "sign_transaction") {
try {
const parsedTxData = JSON.parse(data.data) as NebulaTxData;
params.handleStream({
data: parsedTxData,
event: "action",
request_id: data.request_id,
type: "sign_transaction",
});
} catch (e) {
console.error("failed to parse action data", e, { event });
}
}
if (data.type === "sign_swap") {
try {
const swapData = JSON.parse(data.data) as NebulaSwapData;
params.handleStream({
data: swapData,
event: "action",
request_id: data.request_id,
type: "sign_swap",
});
} catch (e) {
console.error("failed to parse action data", e, { event });
}
}
break;
}
case "error": {
const data = JSON.parse(event.data) as {
code: number;
error: {
message: string;
};
};
params.handleStream({
data: {
code: data.code,
errorMessage: data.error.message,
},
event: "error",
});
break;
}
case "init": {
const data = JSON.parse(event.data);
params.handleStream({
data: {
request_id: data.request_id,
session_id: data.session_id,
},
event: "init",
});
break;
}
case "context": {
const data = JSON.parse(event.data) as {
data: string;
request_id: string;
session_id: string;
};
const contextData = JSON.parse(data.data) as {
wallet_address: string;
chain_ids: number[];
session_id: string;
};
params.handleStream({
data: contextData,
event: "context",
});
break;
}
case "ping": {
break;
}
default: {
console.warn("unhandled event", event);
}
}
}
}
type ChatStreamedResponse =
| {
event: "init";
data: {
session_id: string;
request_id: string;
};
}
| {
event: "presence";
data: {
session_id: string;
request_id: string;
source: "user" | "reviewer" | (string & {});
data: string;
};
}
| {
event: "delta";
data: {
v: string;
};
}
| {
event: "action";
type: "sign_transaction";
data: NebulaTxData;
request_id: string;
}
| {
event: "action";
type: "sign_swap";
data: NebulaSwapData;
request_id: string;
}
| {
event: "image";
data: {
width: number;
height: number;
url: string;
};
request_id: string;
}
| {
event: "context";
data: {
wallet_address: string;
chain_ids: number[];
session_id: string;
};
}
| {
event: "error";
data: {
code: number;
errorMessage: string;
};
};
type ChatStreamedEvent =
| {
event: "init";
data: string;
}
| {
event: "presence";
data: string;
}
| {
event: "delta";
data: string;
}
| {
event: "image";
data: string;
}
| {
event: "action";
type: "sign_transaction" | "sign_swap";
data: string;
}
| {
event: "context";
data: string;
}
| {
event: "error";
data: string;
}
| {
event: "ping";
data: string;
};
@@ -0,0 +1,45 @@
type SessionContextFilter = {
chain_ids: string[] | null;
wallet_address: string | null;
};
type NebulaUserMessageContentItem =
| {
type: "image";
image_url: string | null;
b64: string | null;
}
| {
type: "text";
text: string;
}
| {
type: "transaction";
transaction_hash: string;
chain_id: number;
};
type NebulaUserMessageContent = NebulaUserMessageContentItem[];
export type NebulaUserMessage = {
role: "user";
content: NebulaUserMessageContent;
};
export type NebulaSessionHistoryMessage =
| {
role: "assistant" | "action" | "image";
content: string;
timestamp: number;
}
| {
role: "user";
content: NebulaUserMessageContent | string;
};
export type NebulaTxData = {
chainId: number;
data: `0x${string}`;
to: string;
value?: string;
};
@@ -0,0 +1,18 @@
import ThirdwebProvider from "../../../components/thirdweb-provider";
import { THIRDWEB_CLIENT } from "../../../lib/client";
import { ChatPageContent } from "../components/ChatPageContent";
export default function ChatPage() {
return (
<div className="min-h-screen">
<ThirdwebProvider>
<ChatPageContent client={THIRDWEB_CLIENT} type="new-chat" />
</ThirdwebProvider>
</div>
);
}
export const metadata = {
title: "AI Chat API - Playground",
description: "Chat with thirdweb AI for blockchain interactions",
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
import type { ThirdwebClient } from "thirdweb";
import { resolveScheme } from "thirdweb/storage";
export function resolveSchemeWithErrorHandler(options: {
uri: string | undefined;
client: ThirdwebClient;
}) {
if (!options.uri) {
return undefined;
}
try {
// eslint-disable-next-line no-restricted-syntax
return resolveScheme({
client: options.client,
uri: options.uri,
});
} catch (err) {
console.error("error resolving ipfs url", options.uri, err);
return undefined;
}
}
@@ -0,0 +1,108 @@
type ExamplePrompt = {
title: string;
message: string;
interceptedReply?: string;
};
const whatCanThirdwebAiDoReply = `
thirdweb AI is a natural language model with improved blockchain reasoning, autonomous transaction capabilities, and real-time access to the blockchain.
[Learn more about thirdweb AI](https://portal.thirdweb.com/ai/chat)
Here are some example actions you can perform with thirdweb AI:
### Bridge & Swap
Bridge and swap native currencies
- Swap 1 USDC to 1 USDT on the Ethereum Mainnet
- Bridge 0.5 ETH from Ethereum Mainnet to Polygon
### Transfer
Send native and ERC-20 currencies
- Send 0.1 ETH to vitalik.eth
- Transfer 1 USDC to saminacodes.eth on Base
### Deploy
Deploy published contracts
- Deploy a Token ERC20 Contract with name "Hello World" and description "My Hello Contract" on Ethereum.
- Deploy a Split contract with two recipients.
- Deploy an ERC1155 Contract named 'Hello World' with description 'Hello badges on Ethereum'
### Understand
Retrieve information about smart contracts.
- What ERC standards are implemented by contract address 0x59325733eb952a92e069C87F0A6168b29E80627f on Ethereum?
- What functions can I use to mint more of my contract's NFTs?
- What is the total supply of NFTs on 0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e?
### Interact
Query wallet balances, addresses, and token holdings.
- How much ETH is in my wallet?
- What is the wallet address of vitalik.eth?
- Does my wallet hold USDC on Base?
### Explore
Access blockchain-specific data.
- What is the last block on zkSync?
- What is the current gas price on Avalanche C-Chain?
- Can you show me transaction details for 0xdfc450bb39e44bd37c22e0bfd0e5212edbea571e4e534d87b5cbbf06f10b9e04 on Optimism?
### Research
Obtain details about tokens, their addresses, and current prices.
- What is the address of USDC on Ethereum?
- Is there a UNI token on Arbitrum?
- What is the current price of ARB?
### Build
Implement features using Web3 SDKs and tools.
- How can I add a connect wallet button to my web app? I want to support users connecting with both email/social wallets and MetaMask and use smart wallets.
- Can you show me how to claim an NFT from an ERC721 using TypeScript?
- I have an ERC1155 contract from thirdweb. Can you show me how to generate and mint with a signature?
`;
const deployTokenReply = `
Let's create your token! Just name it and I'll get started. I'll take care of the rest.
Add more info for more fun:
- Symbol (e.g. 'HELLO')
- Description (e.g. 'Hello world token deployed by thirdweb AI')
- Total Supply (e.g. 1 million)
- Mint total supply to your wallet (default is true)
- Decimal places (default 18)
- Image URL
If you want, I can generate an image for it too.
`;
const buyUsdcReply = `
Easy. How much USDC?
You can pay with any token, but I'll default to ETH.
`;
const transferEthReply = `
Great! How much ETH and to what address?
`;
export const examplePrompts: ExamplePrompt[] = [
{
interceptedReply: whatCanThirdwebAiDoReply,
message: "Tell me about thirdweb AI's capabilities.",
title: "What can thirdweb AI do?",
},
{
interceptedReply: deployTokenReply,
message: "I'd like to deploy a token.",
title: "Launch a Token",
},
{
interceptedReply: buyUsdcReply,
message: "I want to buy USDC",
title: "Buy USDC",
},
{
message: "Analyze the Uniswap v3 contracts on Ethereum",
title: "Analyze the Uniswap contracts",
},
{
interceptedReply: transferEthReply,
message: "I want to send some ETH",
title: "Send ETH to someone",
},
];
+2
View File
@@ -7,6 +7,7 @@ import { Providers } from "./providers";
import "./globals.css";
import { ThemeProvider } from "next-themes";
import NextTopLoader from "nextjs-toploader";
import { Toaster } from "sonner";
const sansFont = Inter({
subsets: ["latin"],
@@ -57,6 +58,7 @@ export default async function RootLayout({
<Providers>{children}</Providers>
</AppSidebarLayout>
</div>
<Toaster />
</ThemeProvider>
</body>
</html>
+15 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { ArrowLeftRightIcon } from "lucide-react";
import { ArrowLeftRightIcon, BotIcon } from "lucide-react";
import type { ShadcnSidebarLink } from "@/components/blocks/full-width-sidebar-layout";
import { ContractIcon } from "../icons/ContractIcon";
import { InsightIcon } from "../icons/InsightIcon";
@@ -10,6 +10,19 @@ import { TokenIcon } from "../icons/TokenIcon";
import { WalletProductIcon } from "../icons/WalletProductIcon";
import { insightBlueprints } from "./insight/insightBlueprints";
const ai: ShadcnSidebarLink = {
subMenu: {
label: "AI",
icon: BotIcon,
},
links: [
{
href: "/ai/chat",
label: "Chat API",
},
],
};
const wallets: ShadcnSidebarLink = {
subMenu: {
label: "Wallets",
@@ -215,6 +228,7 @@ const insight: ShadcnSidebarLink = {
};
export const sidebarLinks: ShadcnSidebarLink[] = [
ai,
wallets,
transactions,
contracts,
@@ -0,0 +1,122 @@
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
@@ -0,0 +1,19 @@
import { cn } from "@/lib/utils";
interface InlineCodeProps {
code: string;
className?: string;
}
export function InlineCode({ code, className }: InlineCodeProps) {
return (
<code
className={cn(
"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
className,
)}
>
{code}
</code>
);
}
@@ -0,0 +1,222 @@
import Link from "next/link";
import { onlyText } from "react-children-utilities";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { InlineCode } from "@/components/ui/inline-code";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import CodeClient from "../code/code.client";
export const MarkdownRenderer: React.FC<{
markdownText: string;
className?: string;
code?: {
disableCodeHighlight?: boolean;
ignoreFormattingErrors?: boolean;
className?: string;
};
inlineCode?: {
className?: string;
};
p?: {
className?: string;
};
li?: {
className?: string;
};
skipHtml?: boolean;
}> = (markdownProps) => {
const { markdownText, className, code } = markdownProps;
const commonHeadingClassName = "mb-2 leading-5 font-semibold tracking-tight";
return (
<div className={className}>
<ReactMarkdown
components={{
a: (props) => (
<Link
href={props.href ?? "#"}
target="_blank"
{...cleanedProps(props)}
className="mt-4 underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid"
/>
),
code: ({ ...props }) => {
const codeStr = onlyText(props.children);
if (props?.className || codeStr.length > 100) {
if (code?.disableCodeHighlight || !props.className) {
return (
<div className="my-4">
<InlineCode
{...cleanedProps(props)}
className={markdownProps.code?.className}
code={onlyText(props.children).trim()}
/>
</div>
);
}
const language = props.className.replace("language-", "");
return (
<div className="my-4">
<CodeClient
// @ts-expect-error - TODO: fix this
lang={language}
{...cleanedProps(props)}
className={markdownProps.code?.className}
code={onlyText(props.children).trim()}
ignoreFormattingErrors={code?.ignoreFormattingErrors}
/>
</div>
);
}
return (
<InlineCode
className={markdownProps.inlineCode?.className}
code={onlyText(props.children).trim()}
/>
);
},
h1: (props) => (
<h2
className={cn(
commonHeadingClassName,
"mb-3 border-border border-b pb-2 text-xl md:text-2xl",
)}
{...cleanedProps(props)}
/>
),
h2: (props) => (
<h3
{...cleanedProps(props)}
className={cn(
commonHeadingClassName,
"mt-8 mb-3 border-border border-b pb-2 text-lg md:text-xl",
)}
/>
),
h3: (props) => (
<h4
{...cleanedProps(props)}
className={cn(
commonHeadingClassName,
"mt-4 text-base md:text-lg",
)}
/>
),
h4: (props) => (
<h5
{...cleanedProps(props)}
className={cn(commonHeadingClassName, "mt-4 text-lg")}
/>
),
h5: (props) => (
<h6
{...cleanedProps(props)}
className={cn(commonHeadingClassName, "mt-4 text-lg")}
/>
),
h6: (props) => (
<p
{...cleanedProps(props)}
className={cn(commonHeadingClassName, "mt-4 text-lg")}
/>
),
hr: (props) => (
<hr {...cleanedProps(props)} className="my-5 bg-border" />
),
li: ({ children: c, ...props }) => (
<li
className={cn(
markdownProps.li?.className,
"mb-1.5 text-foreground leading-loose [&>p]:m-0",
)}
{...cleanedProps(props)}
>
{c}
</li>
),
ol: (props) => (
<ol
{...cleanedProps(props)}
className="mb-4 list-outside list-decimal pl-5 [&_ol_li:first-of-type]:mt-1.5 [&_ul_li:first-of-type]:mt-1.5"
/>
),
p: (props) => (
<p
className={cn(
markdownProps.p?.className,
"mb-4 text-foreground leading-loose",
)}
{...cleanedProps(props)}
/>
),
strong(props) {
return <strong className="font-medium" {...cleanedProps(props)} />;
},
table: (props) => (
<div className="mb-6">
<TableContainer>
<Table {...cleanedProps(props)} />
</TableContainer>
</div>
),
tbody: (props) => <TableBody {...cleanedProps(props)} />,
td: (props) => (
<TableCell {...cleanedProps(props)} className="text-left" />
),
th: ({ children: c, ...props }) => (
<TableHead
{...cleanedProps(props)}
className="text-left text-foreground"
>
{c}
</TableHead>
),
thead: (props) => <TableHeader {...cleanedProps(props)} />,
tr: (props) => <TableRow {...cleanedProps(props)} />,
ul: (props) => {
return (
<ul
{...cleanedProps(props)}
className="mb-4 list-outside list-disc pl-5 [&_ol_li:first-of-type]:mt-1.5 [&_ul_li:first-of-type]:mt-1.5"
/>
);
},
}}
remarkPlugins={[remarkGfm]}
skipHtml={markdownProps.skipHtml}
>
{markdownText}
</ReactMarkdown>
</div>
);
};
function cleanedProps<T extends object & { node?: unknown }>(
props: T,
): Omit<T, "node"> {
// biome-ignore lint/correctness/noUnusedVariables: explicitly removing node here
const { node, ...rest } = props;
return rest;
}
+6 -1
View File
@@ -1,4 +1,4 @@
import { BookIcon, ZapIcon } from "lucide-react";
import { BookIcon, ExternalLinkIcon, ZapIcon } from "lucide-react";
import type { SideBar } from "@/components/Layouts/DocLayout";
export const sidebar: SideBar = {
@@ -12,6 +12,11 @@ export const sidebar: SideBar = {
href: "/ai/chat",
icon: <ZapIcon />,
},
{
name: "Playground",
href: "https://playground.thirdweb.com/ai/chat",
icon: <ExternalLinkIcon />,
},
{
name: "Streaming Responses",
href: "/ai/chat/streaming",
+7
View File
@@ -1,5 +1,12 @@
# @thirdweb-dev/nebula
## 0.2.33
### Patch Changes
- Updated dependencies [[`4be655d`](https://github.com/thirdweb-dev/js/commit/4be655dd084dd474d021a7a539a0c67e648bb42d)]:
- thirdweb@5.105.33
## 0.2.32
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.32"
"version": "0.2.33"
}
+6
View File
@@ -1,5 +1,11 @@
# thirdweb
## 5.105.33
### Patch Changes
- [#7850](https://github.com/thirdweb-dev/js/pull/7850) [`4be655d`](https://github.com/thirdweb-dev/js/commit/4be655dd084dd474d021a7a539a0c67e648bb42d) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix onDisconnect not being invoked in react native
## 5.105.32
### Patch Changes
+1 -1
View File
@@ -409,5 +409,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.105.32"
"version": "5.105.33"
}
@@ -293,7 +293,7 @@ const ViewFunds = (props: ConnectedModalPropsInner) => {
};
const DisconnectWallet = (props: ConnectedModalProps) => {
const { wallet, account, theme, onClose } = props;
const { wallet, account, theme, onClose, onDisconnect } = props;
const { disconnect } = useDisconnect();
const siweAuth = useSiweAuth(wallet, account, props.auth);
return (
@@ -304,6 +304,10 @@ const DisconnectWallet = (props: ConnectedModalProps) => {
if (siweAuth.isLoggedIn) {
siweAuth.doLogout();
}
onDisconnect?.({
wallet,
account,
});
}}
style={styles.walletMenuRow}
>
+2
View File
@@ -1,5 +1,7 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.130
## 0.2.129
## 0.2.128
+1 -1
View File
@@ -55,5 +55,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.129"
"version": "0.2.130"
}
+28 -62
View File
@@ -190,9 +190,6 @@ importers:
nuqs:
specifier: ^2.4.3
version: 2.4.3([email protected](@babel/[email protected])(@opentelemetry/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected]))([email protected])
p-limit:
specifier: ^6.2.0
version: 6.2.0
papaparse:
specifier: ^5.5.3
version: 5.5.3
@@ -624,6 +621,9 @@ importers:
date-fns:
specifier: 4.1.0
version: 4.1.0
fetch-event-stream:
specifier: 0.1.5
version: 0.1.5
lucide-react:
specifier: 0.525.0
version: 0.525.0([email protected])
@@ -648,21 +648,33 @@ importers:
react:
specifier: 19.1.0
version: 19.1.0
react-children-utilities:
specifier: ^2.10.0
version: 2.10.0([email protected])
react-dom:
specifier: 19.1.0
version: 19.1.0([email protected])
react-hook-form:
specifier: 7.55.0
version: 7.55.0([email protected])
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/[email protected])([email protected])
react-pick-color:
specifier: ^2.0.0
version: 2.0.0([email protected]([email protected]))([email protected])
remark-gfm:
specifier: 4.0.1
version: 4.0.1
server-only:
specifier: ^0.0.1
version: 0.0.1
shiki:
specifier: 1.27.0
version: 1.27.0
sonner:
specifier: 2.0.6
version: 2.0.6([email protected]([email protected]))([email protected])
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
@@ -12356,10 +12368,6 @@ packages:
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
[email protected]:
resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
@@ -15702,10 +15710,10 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
'@aws-sdk/middleware-logger': 3.577.0
'@aws-sdk/middleware-recursion-detection': 3.577.0
@@ -15748,10 +15756,10 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
'@aws-sdk/middleware-logger': 3.577.0
'@aws-sdk/middleware-recursion-detection': 3.577.0
@@ -15794,10 +15802,10 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
'@aws-sdk/middleware-logger': 3.577.0
'@aws-sdk/middleware-recursion-detection': 3.577.0
@@ -15841,7 +15849,7 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
'@aws-sdk/[email protected]':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
@@ -15884,7 +15892,6 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sts'
- aws-crt
'@aws-sdk/[email protected]':
@@ -16021,7 +16028,7 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
@@ -16140,24 +16147,6 @@ snapshots:
'@smithy/util-stream': 4.2.3
tslib: 2.8.1
'@aws-sdk/[email protected](@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/credential-provider-env': 3.587.0
'@aws-sdk/credential-provider-http': 3.587.0
'@aws-sdk/credential-provider-process': 3.587.0
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/[email protected])
'@aws-sdk/types': 3.577.0
'@smithy/credential-provider-imds': 3.2.8
'@smithy/property-provider': 3.1.11
'@smithy/shared-ini-file-loader': 3.1.12
'@smithy/types': 3.7.2
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected])(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sts': 3.592.0
@@ -16212,25 +16201,6 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/credential-provider-env': 3.587.0
'@aws-sdk/credential-provider-http': 3.587.0
'@aws-sdk/credential-provider-ini': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-process': 3.587.0
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/[email protected])
'@aws-sdk/types': 3.577.0
'@smithy/credential-provider-imds': 3.2.8
'@smithy/property-provider': 3.1.11
'@smithy/shared-ini-file-loader': 3.1.12
'@smithy/types': 3.7.2
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- '@aws-sdk/client-sts'
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected])(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/credential-provider-env': 3.587.0
@@ -16504,7 +16474,7 @@ snapshots:
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.1.11
'@smithy/shared-ini-file-loader': 3.1.12
@@ -16640,7 +16610,7 @@ snapshots:
'@babel/[email protected]':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/[email protected]':
dependencies:
@@ -16686,7 +16656,7 @@ snapshots:
'@babel/[email protected]':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -16708,7 +16678,7 @@ snapshots:
'@babel/[email protected]':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/[email protected]': {}
@@ -16733,7 +16703,7 @@ snapshots:
'@babel/[email protected]':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -32461,10 +32431,6 @@ snapshots:
dependencies:
yocto-queue: 1.1.1
[email protected]:
dependencies:
yocto-queue: 1.1.1
[email protected]:
dependencies:
p-limit: 2.3.0