Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿

feat: enhance CreatorCoin component with profile coin fetching and address removal functionality

yoginth.com 0d3702cc aa7758e4

verified
+96 -8
+85 -7
apps/web/src/components/Settings/Pro/CreatorCoin.tsx
··· 4 4 import type { ApolloClientError } from "@hey/types/errors"; 5 5 import { account as accountMetadata } from "@lens-protocol/metadata"; 6 6 import { useQuery } from "@tanstack/react-query"; 7 - import { type GetCoinResponse, getCoin, setApiKey } from "@zoralabs/coins-sdk"; 7 + import { 8 + type GetCoinResponse, 9 + getCoin, 10 + getProfileCoins, 11 + setApiKey 12 + } from "@zoralabs/coins-sdk"; 8 13 import { useCallback, useEffect, useState } from "react"; 9 14 import { toast } from "sonner"; 10 15 import { base } from "viem/chains"; ··· 12 17 import MetaDetails from "@/components/Account/MetaDetails"; 13 18 import { 14 19 Button, 20 + Card, 15 21 Form, 16 22 Image, 17 23 Input, ··· 73 79 onError 74 80 }); 75 81 82 + const savedCreatorCoinAddress = getAccountAttribute( 83 + "creatorCoinAddress", 84 + currentAccount?.metadata?.attributes 85 + ); 86 + 76 87 const form = useZodForm({ 77 88 defaultValues: { 78 - creatorCoinAddress: getAccountAttribute( 79 - "creatorCoinAddress", 80 - currentAccount?.metadata?.attributes 81 - ) 89 + creatorCoinAddress: savedCreatorCoinAddress 82 90 }, 83 91 schema: ValidationSchema 84 92 }); ··· 112 120 queryKey: ["coin", creatorCoinAddress] 113 121 }); 114 122 123 + const { data: creatorCoinFromZora } = useQuery<string | null>({ 124 + enabled: !!currentAccount?.owner && !creatorCoinAddress, 125 + queryFn: async () => { 126 + const res = await getProfileCoins({ 127 + identifier: currentAccount?.owner as string 128 + }); 129 + 130 + return res.data?.profile?.creatorCoin?.address ?? null; 131 + }, 132 + queryKey: ["profileCoins", currentAccount?.owner] 133 + }); 134 + 115 135 const onSubmit = async (data: z.infer<typeof ValidationSchema>) => { 116 136 if (!currentAccount) return; 117 137 ··· 129 149 }); 130 150 }; 131 151 152 + const handleRemove = async () => { 153 + if (!currentAccount) return; 154 + 155 + setIsSubmitting(true); 156 + const preparedAccountMetadata = prepareAccountMetadata(currentAccount, { 157 + attributes: { creatorCoinAddress: undefined } 158 + }); 159 + 160 + const metadataUri = await uploadMetadata( 161 + accountMetadata(preparedAccountMetadata) 162 + ); 163 + 164 + return await setAccountMetadata({ 165 + onCompleted: async () => { 166 + form.setValue("creatorCoinAddress", "", { 167 + shouldDirty: true, 168 + shouldTouch: true, 169 + shouldValidate: true 170 + }); 171 + }, 172 + variables: { request: { metadataUri } } 173 + }); 174 + }; 175 + 132 176 return ( 133 177 <Form className="space-y-3" form={form} onSubmit={onSubmit}> 134 178 <Input ··· 137 181 type="text" 138 182 {...form.register("creatorCoinAddress")} 139 183 /> 184 + {!savedCreatorCoinAddress && creatorCoinFromZora ? ( 185 + <Card className="p-5"> 186 + <div className="space-y-1"> 187 + <div>You have a creator coin available to set</div> 188 + <button 189 + className="text-gray-500 text-sm underline" 190 + onClick={() => { 191 + form.setValue("creatorCoinAddress", creatorCoinFromZora, { 192 + shouldDirty: true, 193 + shouldTouch: true, 194 + shouldValidate: true 195 + }); 196 + }} 197 + type="button" 198 + > 199 + {creatorCoinFromZora} 200 + </button> 201 + </div> 202 + </Card> 203 + ) : null} 140 204 {isValidAddress && ( 141 205 <> 142 206 {isFetchingCoin && ( ··· 164 228 )} 165 229 </> 166 230 )} 167 - <div className="flex justify-end"> 231 + <div className="flex space-x-2"> 232 + {savedCreatorCoinAddress ? ( 233 + <Button 234 + className="w-full" 235 + disabled={ 236 + isSubmitting || (!creatorCoinAddress && !savedCreatorCoinAddress) 237 + } 238 + loading={isSubmitting} 239 + onClick={handleRemove} 240 + outline 241 + type="button" 242 + > 243 + Remove 244 + </Button> 245 + ) : null} 168 246 <Button 169 - className="ml-auto" 247 + className="w-full" 170 248 disabled={ 171 249 isSubmitting || !form.formState.isDirty || !isValidAddress || !coin 172 250 }
+11 -1
apps/web/src/helpers/prepareAccountMetadata.ts
··· 44 44 value 45 45 })) ?? []; 46 46 47 + const keysToDelete = new Set( 48 + Object.entries(attrs) 49 + .filter(([, v]) => v === undefined) 50 + .map(([key]) => key) 51 + ); 52 + 47 53 const newAttrs: MetadataAttribute[] = Object.entries(attrs) 48 54 .filter(([, v]) => v !== undefined) 49 55 .map(([key, value]) => ({ ··· 56 62 const finalBio = bio || current.metadata?.bio || undefined; 57 63 58 64 const mergedByKey = new Map<string, MetadataAttribute>( 59 - [...prevAttrs, ...newAttrs].map((a) => [a.key, a]) 65 + prevAttrs.filter((a) => !keysToDelete.has(a.key)).map((a) => [a.key, a]) 60 66 ); 67 + 68 + for (const a of newAttrs) { 69 + mergedByKey.set(a.key, a); 70 + } 61 71 const mergedAttrs = Array.from(mergedByKey.values()); 62 72 63 73 const prepared: AccountOptions = {