Skip to content

Commit 5ab59ff

Browse files
committed
Dashboard: Add token selector in asset creation wizard (#7132)
<!-- ## 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 token sale functionality within the application. It introduces new properties, refines existing structures, and improves the token selection process to accommodate native tokens and better handle token sales. ### Detailed summary - Added `saleTokenAddress` to the form validation schema. - Updated `CreateTokenAssetPage` to handle native token address logic. - Changed `TokenSelector` to support selected tokens and improved token address handling. - Enhanced `TokenSaleSection` to include a currency field for token sales. - Refined state management in `PaymentLinkForm` for better token handling. - Modified `TokenInfoFieldset` to trigger updates on chain changes. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 73629e9 commit 5ab59ff

File tree

12 files changed

+201
-154
lines changed

12 files changed

+201
-154
lines changed

apps/dashboard/src/@/components/blocks/TokenSelector.stories.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,28 @@ function Variant(props: {
3535
label: string;
3636
selectedChainId?: number;
3737
}) {
38-
const [tokenAddress, setTokenAddress] = useState<string>("");
38+
const [token, setToken] = useState<
39+
| {
40+
address: string;
41+
chainId: number;
42+
}
43+
| undefined
44+
>(undefined);
45+
3946
return (
4047
<BadgeContainer label={props.label}>
4148
<TokenSelector
42-
tokenAddress={tokenAddress}
49+
addNativeTokenIfMissing={false}
50+
showCheck={false}
51+
selectedToken={token}
4352
chainId={props.selectedChainId}
4453
client={storybookThirdwebClient}
45-
onChange={setTokenAddress}
54+
onChange={(v) => {
55+
setToken({
56+
address: v.address,
57+
chainId: v.chainId,
58+
});
59+
}}
4660
/>
4761
</BadgeContainer>
4862
);

apps/dashboard/src/@/components/blocks/TokenSelector.tsx

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,103 @@
11
import { useCallback, useMemo } from "react";
2-
import type { ThirdwebClient } from "thirdweb";
2+
import {
3+
NATIVE_TOKEN_ADDRESS,
4+
type ThirdwebClient,
5+
getAddress,
6+
} from "thirdweb";
37
import { shortenAddress } from "thirdweb/utils";
8+
import { useAllChainsData } from "../../../hooks/chains/allChains";
49
import { useTokensData } from "../../../hooks/tokens/tokens";
510
import { replaceIpfsUrl } from "../../../lib/sdk";
611
import { fallbackChainIcon } from "../../../utils/chain-icons";
12+
import type { TokenMetadata } from "../../api/universal-bridge/tokens";
713
import { cn } from "../../lib/utils";
814
import { Badge } from "../ui/badge";
915
import { Img } from "./Img";
1016
import { SelectWithSearch } from "./select-with-search";
1117

1218
type Option = { label: string; value: string };
1319

20+
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
21+
1422
export function TokenSelector(props: {
15-
tokenAddress: string | undefined;
16-
onChange: (tokenAddress: string) => void;
23+
selectedToken: { chainId: number; address: string } | undefined;
24+
onChange: (token: TokenMetadata) => void;
1725
className?: string;
1826
popoverContentClassName?: string;
1927
chainId?: number;
2028
side?: "left" | "right" | "top" | "bottom";
21-
disableChainId?: boolean;
29+
disableAddress?: boolean;
2230
align?: "center" | "start" | "end";
2331
placeholder?: string;
2432
client: ThirdwebClient;
2533
disabled?: boolean;
2634
enabled?: boolean;
35+
showCheck: boolean;
36+
addNativeTokenIfMissing: boolean;
2737
}) {
28-
const { tokens, isFetching } = useTokensData({
38+
const tokensQuery = useTokensData({
2939
chainId: props.chainId,
3040
enabled: props.enabled,
3141
});
3242

43+
const { idToChain } = useAllChainsData();
44+
45+
const tokens = useMemo(() => {
46+
if (!tokensQuery.data) {
47+
return [];
48+
}
49+
50+
if (props.addNativeTokenIfMissing) {
51+
const hasNativeToken = tokensQuery.data.some(
52+
(token) => token.address === checksummedNativeTokenAddress,
53+
);
54+
55+
if (!hasNativeToken && props.chainId) {
56+
return [
57+
{
58+
name:
59+
idToChain.get(props.chainId)?.nativeCurrency.name ??
60+
"Native Token",
61+
symbol:
62+
idToChain.get(props.chainId)?.nativeCurrency.symbol ?? "ETH",
63+
decimals: 18,
64+
chainId: props.chainId,
65+
address: checksummedNativeTokenAddress,
66+
} satisfies TokenMetadata,
67+
...tokensQuery.data,
68+
];
69+
}
70+
}
71+
return tokensQuery.data;
72+
}, [
73+
tokensQuery.data,
74+
props.chainId,
75+
props.addNativeTokenIfMissing,
76+
idToChain,
77+
]);
78+
79+
const addressChainToToken = useMemo(() => {
80+
const value = new Map<string, TokenMetadata>();
81+
for (const token of tokens) {
82+
value.set(`${token.chainId}:${token.address}`, token);
83+
}
84+
return value;
85+
}, [tokens]);
86+
3387
const options = useMemo(() => {
34-
return tokens.allTokens.map((token) => {
35-
return {
36-
label: token.symbol,
37-
value: `${token.chainId}:${token.address}`,
38-
};
39-
});
40-
}, [tokens.allTokens]);
88+
return (
89+
tokens.map((token) => {
90+
return {
91+
label: token.symbol,
92+
value: `${token.chainId}:${token.address}`,
93+
};
94+
}) || []
95+
);
96+
}, [tokens]);
4197

4298
const searchFn = useCallback(
4399
(option: Option, searchValue: string) => {
44-
const token = tokens.addressChainToToken.get(option.value);
100+
const token = addressChainToToken.get(option.value);
45101
if (!token) {
46102
return false;
47103
}
@@ -55,12 +111,12 @@ export function TokenSelector(props: {
55111
token.address.toLowerCase().includes(searchValue.toLowerCase())
56112
);
57113
},
58-
[tokens],
114+
[addressChainToToken],
59115
);
60116

61117
const renderOption = useCallback(
62118
(option: Option) => {
63-
const token = tokens.addressChainToToken.get(option.value);
119+
const token = addressChainToToken.get(option.value);
64120
if (!token) {
65121
return option.label;
66122
}
@@ -87,36 +143,46 @@ export function TokenSelector(props: {
87143
{token.symbol}
88144
</span>
89145

90-
{!props.disableChainId && (
91-
<Badge variant="outline" className="gap-2 max-sm:hidden">
146+
{!props.disableAddress && (
147+
<Badge variant="outline" className="gap-2 py-1 max-sm:hidden">
92148
<span className="text-muted-foreground">Address</span>
93149
{shortenAddress(token.address, 4)}
94150
</Badge>
95151
)}
96152
</div>
97153
);
98154
},
99-
[tokens, props.disableChainId, props.client],
155+
[addressChainToToken, props.disableAddress, props.client],
100156
);
101157

158+
const selectedValue = props.selectedToken
159+
? `${props.selectedToken.chainId}:${props.selectedToken.address}`
160+
: undefined;
161+
102162
return (
103163
<SelectWithSearch
104164
searchPlaceholder="Search by name or symbol"
105-
value={props.tokenAddress}
165+
value={selectedValue}
106166
options={options}
107167
onValueChange={(tokenAddress) => {
108-
props.onChange(tokenAddress);
168+
const token = addressChainToToken.get(tokenAddress);
169+
if (!token) {
170+
return;
171+
}
172+
props.onChange(token);
109173
}}
110174
closeOnSelect={true}
111-
showCheck={false}
175+
showCheck={props.showCheck}
112176
placeholder={
113-
isFetching ? "Loading Tokens..." : props.placeholder || "Select Token"
177+
tokensQuery.isPending
178+
? "Loading Tokens..."
179+
: props.placeholder || "Select Token"
114180
}
115181
overrideSearchFn={searchFn}
116182
renderOption={renderOption}
117183
className={props.className}
118184
popoverContentClassName={props.popoverContentClassName}
119-
disabled={isFetching || props.disabled}
185+
disabled={tokensQuery.isPending || props.disabled}
120186
side={props.side}
121187
align={props.align}
122188
/>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/create-token-page-impl.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { defineDashboardChain } from "lib/defineDashboardChain";
1010
import { useRef } from "react";
1111
import { toast } from "sonner";
1212
import {
13+
NATIVE_TOKEN_ADDRESS,
1314
type ThirdwebClient,
15+
getAddress,
1416
getContract,
1517
sendAndConfirmTransaction,
1618
toUnits,
@@ -286,6 +288,11 @@ export function CreateTokenAssetPage(props: {
286288
formValues.saleEnabled && salePercent > 0
287289
? formValues.salePrice
288290
: "0",
291+
currencyAddress:
292+
getAddress(formValues.saleTokenAddress) ===
293+
getAddress(NATIVE_TOKEN_ADDRESS)
294+
? undefined
295+
: formValues.saleTokenAddress,
289296
startTime: new Date(),
290297
metadata: {
291298
name:

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/create-token-page.client.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
22

3-
import {} from "@/components/blocks/multi-step-status/multi-step-status";
4-
import {} from "@/components/ui/dialog";
53
import { zodResolver } from "@hookform/resolvers/zod";
6-
import {} from "lucide-react";
74
import { useState } from "react";
85
import { useForm } from "react-hook-form";
9-
import type { ThirdwebClient } from "thirdweb";
6+
import {
7+
NATIVE_TOKEN_ADDRESS,
8+
type ThirdwebClient,
9+
getAddress,
10+
} from "thirdweb";
1011
import { TokenDistributionFieldset } from "./distribution/token-distribution";
1112
import {
1213
type CreateAssetFormValues,
@@ -27,6 +28,8 @@ export type CreateTokenFunctions = {
2728
airdropTokens: (values: CreateAssetFormValues) => Promise<void>;
2829
};
2930

31+
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
32+
3033
export function CreateTokenAssetPageUI(props: {
3134
accountAddress: string;
3235
client: ThirdwebClient;
@@ -64,6 +67,7 @@ export function CreateTokenAssetPageUI(props: {
6467
values: {
6568
// sale fieldset
6669
saleAllocationPercentage: "0",
70+
saleTokenAddress: checksummedNativeTokenAddress,
6771
salePrice: "0.1",
6872
supply: "1000000",
6973
saleEnabled: false,
@@ -78,6 +82,13 @@ export function CreateTokenAssetPageUI(props: {
7882
<div>
7983
{step === "token-info" && (
8084
<TokenInfoFieldset
85+
onChainUpdated={() => {
86+
// if the chain is updated, set the sale token address to the native token address
87+
tokenDistributionForm.setValue(
88+
"saleTokenAddress",
89+
checksummedNativeTokenAddress,
90+
);
91+
}}
8192
client={props.client}
8293
form={tokenInfoForm}
8394
onNext={() => {
@@ -88,6 +99,7 @@ export function CreateTokenAssetPageUI(props: {
8899

89100
{step === "distribution" && (
90101
<TokenDistributionFieldset
102+
client={props.client}
91103
form={tokenDistributionForm}
92104
accountAddress={props.accountAddress}
93105
chainId={tokenInfoForm.watch("chain")}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/distribution/token-distribution.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
type Segment,
77
} from "@/components/blocks/distribution-chart";
88
import { Input } from "@/components/ui/input";
9-
import {} from "lucide-react";
9+
import type { ThirdwebClient } from "thirdweb";
1010
import { Form } from "../../../../../../../../@/components/ui/form";
1111
import { StepCard } from "../create-token-card";
1212
import type {
@@ -22,6 +22,7 @@ export function TokenDistributionFieldset(props: {
2222
onPrevious: () => void;
2323
form: TokenDistributionForm;
2424
chainId: string;
25+
client: ThirdwebClient;
2526
}) {
2627
const { form } = props;
2728

@@ -60,7 +61,11 @@ export function TokenDistributionFieldset(props: {
6061
</div>
6162

6263
<TokenAirdropSection form={form} />
63-
<TokenSaleSection form={form} chainId={props.chainId} />
64+
<TokenSaleSection
65+
form={form}
66+
chainId={props.chainId}
67+
client={props.client}
68+
/>
6469
</div>
6570
</StepCard>
6671
</form>

0 commit comments

Comments
 (0)