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

feat: implement CreatorCoin component in Pro settings for managing creator coin address

yoginth.com e3bc6e4d d9d0dd90

verified
+200 -55
+5 -5
apps/web/src/components/Account/CreatorCoin/index.tsx
··· 5 5 import { useState } from "react"; 6 6 import type { Address } from "viem"; 7 7 import { base } from "viem/chains"; 8 + import getAccountAttribute from "@/helpers/getAccountAttribute"; 8 9 import { Image, Modal } from "../../Shared/UI"; 9 10 import MetaDetails from "../MetaDetails"; 10 11 import CreatorCoinDetails from "./CreatorCoinDetails"; ··· 17 18 18 19 const CreatorCoin = ({ account }: CreatorCoinProps) => { 19 20 const [showModal, setShowModal] = useState(false); 20 - // const creatorCoinAddress = getAccountAttribute( 21 - // "creatorCoinAddress", 22 - // account?.metadata?.attributes 23 - // ); 24 - const creatorCoinAddress = "0x58b14cc0ebb0ce5387557adbe6477e001d3dcde0"; 21 + const creatorCoinAddress = getAccountAttribute( 22 + "creatorCoinAddress", 23 + account?.metadata?.attributes 24 + ); 25 25 26 26 const { data: coin } = useQuery<GetCoinResponse["zora20Token"] | null>({ 27 27 enabled: !!creatorCoinAddress,
+13 -50
apps/web/src/components/Settings/Personalize/Form.tsx
··· 1 1 import { MeVariables } from "@hey/data/constants"; 2 2 import { ERRORS } from "@hey/data/errors"; 3 3 import { Regex } from "@hey/data/regex"; 4 - import trimify from "@hey/helpers/trimify"; 5 4 import { useMeLazyQuery, useSetAccountMetadataMutation } from "@hey/indexer"; 6 5 import type { ApolloClientError } from "@hey/types/errors"; 7 - import type { 8 - AccountOptions, 9 - MetadataAttribute 10 - } from "@lens-protocol/metadata"; 11 - import { 12 - account as accountMetadata, 13 - MetadataAttributeType 14 - } from "@lens-protocol/metadata"; 6 + import { account as accountMetadata } from "@lens-protocol/metadata"; 15 7 import { useCallback, useState } from "react"; 16 8 import { toast } from "sonner"; 17 9 import { z } from "zod"; ··· 29 21 } from "@/components/Shared/UI"; 30 22 import errorToast from "@/helpers/errorToast"; 31 23 import getAccountAttribute from "@/helpers/getAccountAttribute"; 24 + import prepareAccountMetadata from "@/helpers/prepareAccountMetadata"; 32 25 import uploadMetadata from "@/helpers/uploadMetadata"; 33 26 import useTransactionLifecycle from "@/hooks/useTransactionLifecycle"; 34 27 import useWaitForTransactionToComplete from "@/hooks/useWaitForTransactionToComplete"; ··· 126 119 } 127 120 128 121 setIsSubmitting(true); 129 - const otherAttributes = 130 - currentAccount.metadata?.attributes 131 - ?.filter( 132 - (attr) => 133 - !["app", "location", "timestamp", "website", "x"].includes(attr.key) 134 - ) 135 - .map(({ key, type, value }) => ({ 136 - key, 137 - type: MetadataAttributeType[type] as any, 138 - value 139 - })) || []; 140 - 141 - const preparedAccountMetadata: AccountOptions = { 142 - ...(data.name && { name: data.name }), 143 - ...(data.bio && { bio: data.bio }), 144 - attributes: [ 145 - ...(otherAttributes as MetadataAttribute[]), 146 - { 147 - key: "location", 148 - type: MetadataAttributeType.STRING, 149 - value: data.location 150 - }, 151 - { 152 - key: "website", 153 - type: MetadataAttributeType.STRING, 154 - value: data.website 155 - }, 156 - { key: "x", type: MetadataAttributeType.STRING, value: data.x }, 157 - { 158 - key: "timestamp", 159 - type: MetadataAttributeType.STRING, 160 - value: new Date().toISOString() 161 - } 162 - ], 163 - coverPicture: coverUrl || undefined, 164 - picture: avatarUrl || undefined 165 - }; 166 - preparedAccountMetadata.attributes = 167 - preparedAccountMetadata.attributes?.filter((m) => { 168 - return m.key !== "" && Boolean(trimify(m.value)); 169 - }); 122 + const preparedAccountMetadata = prepareAccountMetadata(currentAccount, { 123 + attributes: { 124 + location: data.location, 125 + website: data.website, 126 + x: data.x 127 + }, 128 + bio: data.bio, 129 + coverPicture: coverUrl, 130 + name: data.name, 131 + picture: avatarUrl 132 + }); 170 133 const metadataUri = await uploadMetadata( 171 134 accountMetadata(preparedAccountMetadata) 172 135 );
+103
apps/web/src/components/Settings/Pro/CreatorCoin.tsx
··· 1 + import { Regex } from "@hey/data/regex"; 2 + import { useSetAccountMetadataMutation } from "@hey/indexer"; 3 + import type { ApolloClientError } from "@hey/types/errors"; 4 + import { account as accountMetadata } from "@lens-protocol/metadata"; 5 + import { useCallback, useState } from "react"; 6 + import { z } from "zod"; 7 + import { Button, Form, Input, useZodForm } from "@/components/Shared/UI"; 8 + import errorToast from "@/helpers/errorToast"; 9 + import getAccountAttribute from "@/helpers/getAccountAttribute"; 10 + import prepareAccountMetadata from "@/helpers/prepareAccountMetadata"; 11 + import uploadMetadata from "@/helpers/uploadMetadata"; 12 + import useTransactionLifecycle from "@/hooks/useTransactionLifecycle"; 13 + import useWaitForTransactionToComplete from "@/hooks/useWaitForTransactionToComplete"; 14 + import { useAccountStore } from "@/store/persisted/useAccountStore"; 15 + 16 + const ValidationSchema = z.object({ 17 + creatorCoinAddress: z.union([ 18 + z.string().regex(Regex.evmAddress, { message: "Invalid address" }), 19 + z.string().max(0) 20 + ]) 21 + }); 22 + 23 + const CreatorCoin = () => { 24 + const { currentAccount } = useAccountStore(); 25 + const [isSubmitting, setIsSubmitting] = useState(false); 26 + const handleTransactionLifecycle = useTransactionLifecycle(); 27 + const waitForTransactionToComplete = useWaitForTransactionToComplete(); 28 + 29 + const onCompleted = async (hash: string) => { 30 + await waitForTransactionToComplete(hash); 31 + location.reload(); 32 + }; 33 + 34 + const onError = useCallback((error: ApolloClientError) => { 35 + setIsSubmitting(false); 36 + errorToast(error); 37 + }, []); 38 + 39 + const [setAccountMetadata] = useSetAccountMetadataMutation({ 40 + onCompleted: async ({ setAccountMetadata }) => { 41 + if (setAccountMetadata.__typename === "SetAccountMetadataResponse") { 42 + return await onCompleted(setAccountMetadata.hash); 43 + } 44 + 45 + return await handleTransactionLifecycle({ 46 + onCompleted, 47 + onError, 48 + transactionData: setAccountMetadata 49 + }); 50 + }, 51 + onError 52 + }); 53 + 54 + const form = useZodForm({ 55 + defaultValues: { 56 + creatorCoinAddress: getAccountAttribute( 57 + "creatorCoinAddress", 58 + currentAccount?.metadata?.attributes 59 + ) 60 + }, 61 + schema: ValidationSchema 62 + }); 63 + 64 + const onSubmit = async (data: z.infer<typeof ValidationSchema>) => { 65 + if (!currentAccount) return; 66 + 67 + setIsSubmitting(true); 68 + const preparedAccountMetadata = prepareAccountMetadata(currentAccount, { 69 + attributes: { creatorCoinAddress: data.creatorCoinAddress } 70 + }); 71 + 72 + const metadataUri = await uploadMetadata( 73 + accountMetadata(preparedAccountMetadata) 74 + ); 75 + 76 + return await setAccountMetadata({ 77 + variables: { request: { metadataUri } } 78 + }); 79 + }; 80 + 81 + return ( 82 + <Form className="space-y-3" form={form} onSubmit={onSubmit}> 83 + <Input 84 + label="Creator Coin Address" 85 + placeholder="0x..." 86 + type="text" 87 + {...form.register("creatorCoinAddress")} 88 + /> 89 + <div className="flex justify-end"> 90 + <Button 91 + className="ml-auto" 92 + disabled={isSubmitting || !form.formState.isDirty} 93 + loading={isSubmitting} 94 + type="submit" 95 + > 96 + Save 97 + </Button> 98 + </div> 99 + </Form> 100 + ); 101 + }; 102 + 103 + export default CreatorCoin;
+3
apps/web/src/components/Settings/Pro/index.tsx
··· 5 5 import { Card, CardHeader } from "@/components/Shared/UI"; 6 6 import { useAccountStore } from "@/store/persisted/useAccountStore"; 7 7 import BetaToggle from "./BetaToggle"; 8 + import CreatorCoin from "./CreatorCoin"; 8 9 import DefaultToNameToggle from "./DefaultToNameToggle"; 9 10 10 11 const ProSettings = () => { ··· 22 23 <div className="space-y-5 p-5"> 23 24 <BetaToggle /> 24 25 <DefaultToNameToggle /> 26 + <div className="divider" /> 27 + <CreatorCoin /> 25 28 </div> 26 29 ) : ( 27 30 <ProFeatureNotice className="m-5" feature="pro settings" />
+76
apps/web/src/helpers/prepareAccountMetadata.ts
··· 1 + import trimify from "@hey/helpers/trimify"; 2 + import type { 3 + AccountOptions, 4 + MetadataAttribute 5 + } from "@lens-protocol/metadata"; 6 + import { MetadataAttributeType } from "@lens-protocol/metadata"; 7 + 8 + type ExistingAttribute = { 9 + key: string; 10 + type: string; 11 + value: string; 12 + }; 13 + 14 + type ExistingMetadata = { 15 + name?: string | null; 16 + bio?: string | null; 17 + picture?: string | null; 18 + coverPicture?: string | null; 19 + attributes?: ExistingAttribute[] | null; 20 + }; 21 + 22 + type HasMetadata = { 23 + metadata?: ExistingMetadata | null; 24 + }; 25 + 26 + interface PrepareAccountMetadataInput { 27 + attributes: Record<string, string | undefined>; 28 + name?: string; 29 + bio?: string; 30 + picture?: string | undefined; 31 + coverPicture?: string | undefined; 32 + } 33 + 34 + const prepareAccountMetadata = ( 35 + current: HasMetadata, 36 + input: PrepareAccountMetadataInput 37 + ): AccountOptions => { 38 + const { name, bio, picture, coverPicture, attributes: attrs } = input; 39 + 40 + const prevAttrs: MetadataAttribute[] = 41 + current.metadata?.attributes?.map(({ key, type, value }) => ({ 42 + key, 43 + type: (MetadataAttributeType as any)[type], 44 + value 45 + })) ?? []; 46 + 47 + const newAttrs: MetadataAttribute[] = Object.entries(attrs) 48 + .filter(([, v]) => v !== undefined) 49 + .map(([key, value]) => ({ 50 + key, 51 + type: MetadataAttributeType.STRING, 52 + value: value as string 53 + })); 54 + 55 + const finalName = name || current.metadata?.name || undefined; 56 + const finalBio = bio || current.metadata?.bio || undefined; 57 + 58 + const mergedByKey = new Map<string, MetadataAttribute>( 59 + [...prevAttrs, ...newAttrs].map((a) => [a.key, a]) 60 + ); 61 + const mergedAttrs = Array.from(mergedByKey.values()); 62 + 63 + const prepared: AccountOptions = { 64 + ...(finalName ? { name: finalName } : {}), 65 + ...(finalBio ? { bio: finalBio } : {}), 66 + attributes: mergedAttrs.filter( 67 + (a) => a.key !== "" && Boolean(trimify(a.value)) 68 + ), 69 + coverPicture: coverPicture ?? current.metadata?.coverPicture ?? undefined, 70 + picture: picture ?? current.metadata?.picture ?? undefined 71 + }; 72 + 73 + return prepared; 74 + }; 75 + 76 + export default prepareAccountMetadata;