├── theme.js ├── .eslintrc.json ├── .example.env.local ├── context.js ├── public ├── favicon.ico ├── close.svg ├── vercel.svg ├── create-post.svg └── icon.svg ├── components ├── index.js ├── Button.js ├── Placeholders.js ├── SearchInput.js └── CreatePostModal.js ├── next.config.js ├── styles ├── globals.css └── Home.module.css ├── .gitignore ├── package.json ├── README.md ├── utils.js ├── api ├── index.js ├── mutations.js └── queries.js ├── pages ├── edit-profile.js ├── profiles.js ├── index.js ├── _app.js └── profile │ └── [id].js └── abi ├── lensperiphery.json └── lenshub.json /theme.js: -------------------------------------------------------------------------------- 1 | export const PINK = "249, 92, 255" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.example.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PROJECT_ID=XXX 2 | NEXT_PUBLIC_PROJECT_SECRET=XXX -------------------------------------------------------------------------------- /context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export const AppContext = createContext() -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/lens-protocol-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | export { Button } from './Button' 2 | export { SearchInput } from './SearchInput' 3 | export { Placeholders } from './Placeholders' -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: [ 6 | 'ipfs.infura.io', 7 | 'statics-polygon-lens-staging.s3.eu-west-1.amazonaws.com', 8 | 'lens.infura-ipfs.io', 9 | "" 10 | ], 11 | }, 12 | } 13 | 14 | module.exports = nextConfig 15 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); 2 | 3 | html, 4 | body { 5 | padding: 0; 6 | margin: 0; 7 | background-color: rgba(0, 0, 0, .015); 8 | min-height: 100vh; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | font-family: 'Inter', sans-serif; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /components/Button.js: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | import { PINK } from '../theme' 3 | 4 | export function Button({ 5 | buttonText, 6 | onClick 7 | }) { 8 | return ( 9 | 13 | ) 14 | } 15 | 16 | const buttonStyle = css` 17 | border: none; 18 | outline: none; 19 | margin-left: 15px; 20 | color: #340036; 21 | padding: 17px; 22 | border-radius: 25px; 23 | cursor: pointer; 24 | font-size: 14px; 25 | font-weight: 500; 26 | background-color: rgb(${PINK}); 27 | transition: all .35s; 28 | width: 240px; 29 | letter-spacing: .75px; 30 | &:hover { 31 | background-color: rgba(${PINK}, .75); 32 | } 33 | ` -------------------------------------------------------------------------------- /components/Placeholders.js: -------------------------------------------------------------------------------- 1 | import { css, keyframes } from '@emotion/css' 2 | 3 | export function Placeholders({ 4 | number 5 | }) { 6 | const rows = [] 7 | for (let i = 0; i < number; i ++) { 8 | rows.push( 9 |
13 | ) 14 | } 15 | return rows 16 | } 17 | 18 | const shimmer = keyframes` 19 | from { 20 | opacity: .2; 21 | } 22 | 23 | 50% { 24 | opacity: 1; 25 | } 26 | 27 | 100% { 28 | opacity: .2; 29 | } 30 | ` 31 | 32 | const grayLoadingStyle = css` 33 | background-color: rgba(0, 0, 0, .1); 34 | height: 115px; 35 | width: 100%; 36 | margin-top: 13px; 37 | border-radius: 7px; 38 | animation: ${shimmer} 2s infinite linear; 39 | ` -------------------------------------------------------------------------------- /components/SearchInput.js: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css' 2 | 3 | export function SearchInput({ 4 | placeholder, onChange, value, onKeyDown = null 5 | }) { 6 | return ( 7 | 14 | ) 15 | } 16 | 17 | const inputStyle = css` 18 | outline: none; 19 | border: none; 20 | padding: 15px 20px; 21 | font-size: 16px; 22 | border-radius: 25px; 23 | border: 2px solid rgba(0, 0, 0, .04); 24 | transition: all .4s; 25 | width: 300px; 26 | background-color: #fafafa; 27 | &:focus { 28 | background-color: white; 29 | border: 2px solid rgba(0, 0, 0, .1); 30 | } 31 | ` 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lens-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/css": "^11.9.0", 13 | "@rainbow-me/rainbowkit": "^0.1.0", 14 | "@urql/exchange-auth": "^0.1.7", 15 | "date-fns": "^2.28.0", 16 | "ethers": "^5.6.6", 17 | "graphql": "^16.5.0", 18 | "ipfs-http-client": "^59.0.0", 19 | "next": "12.1.6", 20 | "omit-deep": "^0.3.0", 21 | "react": "18.1.0", 22 | "react-dom": "18.1.0", 23 | "react-markdown": "^8.0.3", 24 | "react-modal": "^3.15.1", 25 | "urql": "^2.2.0", 26 | "uuid": "^8.3.2", 27 | "wagmi": "^0.4.4" 28 | }, 29 | "devDependencies": { 30 | "eslint": "8.15.0", 31 | "eslint-config-next": "12.1.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/close.svg: -------------------------------------------------------------------------------- 1 | 9.12Created with Sketch. -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/create-post.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lens Protocol Front End Starter 2 | 3 | This is an example of how to build a front-end application on top of [Lens Protocol](https://docs.lens.xyz/docs). 4 | 5 | The main API calls used in this app are defined in __api/index.js__: 6 | 7 | 1. [recommendProfiles](https://docs.lens.xyz/docs/recommended-profiles#api-details) - Get popular profiles 8 | 9 | 2. [getProfiles](https://docs.lens.xyz/docs/get-profiles) - Get profiles by passing in an array of `profileIds` 10 | 11 | 3. [getPublications](https://docs.lens.xyz/docs/get-publications) - Returns a list of publications based on your request query 12 | 13 | 4. [searchProfiles](https://docs.lens.xyz/docs/search-profiles-and-publications) - Allows a user to search across hashtags on publications or profile handles. This query returns either a Post and Comment or Profile. 14 | 15 | 5. [follow](https://docs.lens.xyz/docs/functions#follow) - Follow a user 16 | 17 | 6. [burn](https://docs.lens.xyz/docs/functions#burn) - Unfollows a user 18 | 19 | 7. [timeline](https://docs.lens.xyz/docs/user-timeline) - Shows a feed of content tailored to a signed in user 20 | 21 | 8. [createSetProfileMetadataTypedData](https://docs.lens.xyz/docs/create-set-update-profile-metadata-typed-data) - Allows a user to update the metadata URI for their profile 22 | 23 | 9. [post](https://docs.lens.xyz/docs/functions#post) - Allows a user to publish content 24 | 25 | You can view all of the APIs [here](https://docs.lens.xyz/docs/introduction) and contract methods [here](https://docs.lens.xyz/docs/functions) 26 | 27 | ## Running this project 28 | 29 | > For this project to run, you must configure the **Infura project ID and project secret** in a file named `.env.local`. Check out .example.env.local for guidance. 30 | 31 | You can run this project by following these steps: 32 | 33 | 1. Clone the repo, change into the directory, and install the dependencies 34 | 35 | ```sh 36 | git clone git@github.com:dabit3/lens-protocol-frontend.git 37 | 38 | cd lens-protocol-frontend 39 | 40 | npm install 41 | 42 | # or 43 | 44 | yarn 45 | ``` 46 | 47 | 2. Run the project 48 | 49 | ```sh 50 | npm run dev 51 | ``` 52 | 53 | 3. Open the project in your browser at [localhost:3000](http://localhost:3000/) -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import { basicClient, STORAGE_KEY } from './api' 2 | import { refresh as refreshMutation } from './api/mutations' 3 | import { ethers, utils } from 'ethers' 4 | import omitDeep from 'omit-deep' 5 | 6 | export function trimString(string, length) { 7 | if (!string) return null 8 | return string.length < length ? string : string.substr(0, length-1) + "..." 9 | } 10 | 11 | export function parseJwt (token) { 12 | var base64Url = token.split('.')[1]; 13 | var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 14 | var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { 15 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 16 | }).join('')); 17 | 18 | return JSON.parse(jsonPayload); 19 | }; 20 | 21 | export async function refreshAuthToken() { 22 | const token = JSON.parse(localStorage.getItem(STORAGE_KEY)) 23 | if (!token) return 24 | try { 25 | const authData = await basicClient.mutation(refreshMutation, { 26 | refreshToken: token.refreshToken 27 | }).toPromise() 28 | 29 | if (!authData.data) return 30 | 31 | const { accessToken, refreshToken } = authData.data.refresh 32 | const exp = parseJwt(refreshToken).exp 33 | 34 | localStorage.setItem(STORAGE_KEY, JSON.stringify({ 35 | accessToken, refreshToken, exp 36 | })) 37 | 38 | return { 39 | accessToken 40 | } 41 | } catch (err) { 42 | console.log('error:', err) 43 | } 44 | } 45 | 46 | export function getSigner() { 47 | const provider = new ethers.providers.Web3Provider(window.ethereum) 48 | return provider.getSigner(); 49 | } 50 | 51 | export function signedTypeData (domain, types, value) { 52 | const signer = getSigner(); 53 | return signer._signTypedData( 54 | omitDeep(domain, '__typename'), 55 | omitDeep(types, '__typename'), 56 | omitDeep(value, '__typename') 57 | ) 58 | } 59 | 60 | export function splitSignature(signature) { 61 | return utils.splitSignature(signature) 62 | } 63 | 64 | export function generateRandomColor(){ 65 | let maxVal = 0xFFFFFF; // 16777215 66 | let randomNumber = Math.random() * maxVal; 67 | randomNumber = Math.floor(randomNumber); 68 | randomNumber = randomNumber.toString(16); 69 | let randColor = randomNumber.padStart(6, 0); 70 | return `#${randColor.toUpperCase()}` 71 | } 72 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import { createClient as createUrqlClient } from 'urql' 2 | import { getProfiles, getPublications } from './queries' 3 | import { createPostTypedData } from './mutations' 4 | import { refreshAuthToken, generateRandomColor, signedTypeData } from '../utils' 5 | 6 | export const APIURL = "https://api.lens.dev" 7 | export const STORAGE_KEY = "LH_STORAGE_KEY" 8 | export const LENS_HUB_CONTRACT_ADDRESS = "0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d" 9 | export const PERIPHERY_CONTRACT_ADDRESS = "0xeff187b4190E551FC25a7fA4dFC6cf7fDeF7194f" 10 | 11 | export const basicClient = new createUrqlClient({ 12 | url: APIURL 13 | }) 14 | 15 | export async function fetchProfile(id) { 16 | try { 17 | const urqlClient = await createClient() 18 | const returnedProfile = await urqlClient.query(getProfiles, { id }).toPromise(); 19 | const profileData = returnedProfile.data.profiles.items[0] 20 | profileData.color = generateRandomColor() 21 | const pubs = await urqlClient.query(getPublications, { id, limit: 50 }).toPromise() 22 | return { 23 | profile: profileData, 24 | publications: pubs.data.publications.items 25 | } 26 | } catch (err) { 27 | console.log('error fetching profile...', err) 28 | } 29 | } 30 | 31 | export async function createClient() { 32 | const storageData = JSON.parse(localStorage.getItem(STORAGE_KEY)) 33 | if (storageData) { 34 | try { 35 | const { accessToken } = await refreshAuthToken() 36 | const urqlClient = new createUrqlClient({ 37 | url: APIURL, 38 | fetchOptions: { 39 | headers: { 40 | 'x-access-token': `Bearer ${accessToken}` 41 | }, 42 | }, 43 | }) 44 | return urqlClient 45 | } catch (err) { 46 | return basicClient 47 | } 48 | } else { 49 | return basicClient 50 | } 51 | } 52 | 53 | export async function createPostTypedDataMutation (request, token) { 54 | const { accessToken } = await refreshAuthToken() 55 | const urqlClient = new createUrqlClient({ 56 | url: APIURL, 57 | fetchOptions: { 58 | headers: { 59 | 'x-access-token': `Bearer ${accessToken}` 60 | }, 61 | }, 62 | }) 63 | const result = await urqlClient.mutation(createPostTypedData, { 64 | request 65 | }).toPromise() 66 | 67 | return result.data.createPostTypedData 68 | } 69 | 70 | export const signCreatePostTypedData = async (request, token) => { 71 | const result = await createPostTypedDataMutation(request, token) 72 | const typedData = result.typedData 73 | const signature = await signedTypeData(typedData.domain, typedData.types, typedData.value); 74 | return { result, signature }; 75 | } 76 | 77 | export { 78 | recommendProfiles, 79 | getProfiles, 80 | getDefaultProfile, 81 | getPublications, 82 | searchProfiles, 83 | searchPublications, 84 | explorePublications, 85 | doesFollow, 86 | getChallenge, 87 | timeline 88 | } from './queries' 89 | 90 | export { 91 | followUser, 92 | authenticate, 93 | refresh, 94 | createUnfollowTypedData, 95 | broadcast, 96 | createProfileMetadataTypedData, 97 | createPostTypedData 98 | } from './mutations' -------------------------------------------------------------------------------- /api/mutations.js: -------------------------------------------------------------------------------- 1 | 2 | const followUser = ` 3 | mutation($request: FollowRequest!) { 4 | createFollowTypedData(request: $request) { 5 | id 6 | expiresAt 7 | typedData { 8 | domain { 9 | name 10 | chainId 11 | version 12 | verifyingContract 13 | } 14 | types { 15 | FollowWithSig { 16 | name 17 | type 18 | } 19 | } 20 | value { 21 | nonce 22 | deadline 23 | profileIds 24 | datas 25 | } 26 | } 27 | } 28 | } 29 | ` 30 | 31 | const authenticate = ` 32 | mutation Authenticate( 33 | $address: EthereumAddress! 34 | $signature: Signature! 35 | ) { 36 | authenticate(request: { 37 | address: $address, 38 | signature: $signature 39 | }) { 40 | accessToken 41 | refreshToken 42 | } 43 | } 44 | ` 45 | 46 | const refresh = ` 47 | mutation Refresh( 48 | $refreshToken: Jwt! 49 | ) { 50 | refresh(request: { 51 | refreshToken: $refreshToken 52 | }) { 53 | accessToken 54 | refreshToken 55 | } 56 | } 57 | ` 58 | 59 | const broadcast = ` 60 | mutation Broadcast($request: BroadcastRequest!) { 61 | broadcast(request: $request) { 62 | ... on RelayerResult { 63 | txHash 64 | } 65 | ... on RelayError { 66 | reason 67 | } 68 | } 69 | } 70 | ` 71 | 72 | /* UnfollowRequest 73 | * const unfollowRequestData = { profile: ProfileId! } 74 | */ 75 | 76 | const createUnfollowTypedData = ` 77 | mutation($request: UnfollowRequest!) { 78 | createUnfollowTypedData(request: $request) { 79 | id 80 | expiresAt 81 | typedData { 82 | domain { 83 | name 84 | chainId 85 | version 86 | verifyingContract 87 | } 88 | types { 89 | BurnWithSig { 90 | name 91 | type 92 | } 93 | } 94 | value { 95 | nonce 96 | deadline 97 | tokenId 98 | } 99 | } 100 | } 101 | } 102 | ` 103 | 104 | const createProfileMetadataTypedData = ` 105 | mutation CreateSetProfileMetadataTypedData( 106 | $profileId: ProfileId!, $metadata: Url! 107 | ) { 108 | createSetProfileMetadataTypedData(request: { profileId: $profileId, metadata: $metadata }) { 109 | id 110 | expiresAt 111 | typedData { 112 | types { 113 | SetProfileMetadataURIWithSig { 114 | name 115 | type 116 | } 117 | } 118 | domain { 119 | name 120 | chainId 121 | version 122 | verifyingContract 123 | } 124 | value { 125 | nonce 126 | deadline 127 | profileId 128 | metadata 129 | } 130 | } 131 | } 132 | } 133 | ` 134 | 135 | const createPostTypedData = ` 136 | mutation createPostTypedData($request: CreatePublicPostRequest!) { 137 | createPostTypedData(request: $request) { 138 | id 139 | expiresAt 140 | typedData { 141 | types { 142 | PostWithSig { 143 | name 144 | type 145 | } 146 | } 147 | domain { 148 | name 149 | chainId 150 | version 151 | verifyingContract 152 | } 153 | value { 154 | nonce 155 | deadline 156 | profileId 157 | contentURI 158 | collectModule 159 | collectModuleInitData 160 | referenceModule 161 | referenceModuleInitData 162 | } 163 | } 164 | } 165 | } 166 | ` 167 | 168 | export { 169 | followUser, 170 | authenticate, 171 | refresh, 172 | createUnfollowTypedData, 173 | broadcast, 174 | createProfileMetadataTypedData, 175 | createPostTypedData 176 | } -------------------------------------------------------------------------------- /pages/edit-profile.js: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react' 2 | import { ethers } from 'ethers' 3 | import { AppContext } from '../context' 4 | import { css } from '@emotion/css' 5 | import { PINK } from '../theme' 6 | import { Button } from '../components' 7 | import { v4 as uuid } from 'uuid' 8 | import { create } from 'ipfs-http-client' 9 | import PERIPHERY from '../abi/lensperiphery.json' 10 | import { 11 | createProfileMetadataTypedData, 12 | createClient, 13 | PERIPHERY_CONTRACT_ADDRESS 14 | } from '../api' 15 | import { signedTypeData, splitSignature, getSigner } from '../utils' 16 | 17 | const projectId = process.env.NEXT_PUBLIC_PROJECT_ID 18 | const projectSecret = process.env.NEXT_PUBLIC_PROJECT_SECRET 19 | const auth = 'Basic ' + Buffer.from(projectId + ':' + projectSecret).toString('base64'); 20 | 21 | const client = create({ 22 | host: 'ipfs.infura.io', 23 | port: 5001, 24 | protocol: 'https', 25 | headers: { 26 | authorization: auth, 27 | }, 28 | }) 29 | 30 | export default function EditProfile() { 31 | const [updatedProfile, setUpdatedProfile] = useState() 32 | const context = useContext(AppContext) 33 | const { profile } = context 34 | 35 | async function updateProfile() { 36 | if (!updatedProfile) return 37 | 38 | if (updatedProfile.twitter) { 39 | const index = profile.attributes.findIndex(v => v.key === 'twitter') 40 | profile.attributes[index].value = updatedProfile.twitter 41 | } 42 | 43 | const newMeta = { 44 | name: profile.name, 45 | bio: profile.bio, 46 | attributes: profile.attributes, 47 | version: "1.0.0", 48 | metadata_id: uuid(), 49 | previousMetadata: profile.metadata, 50 | createdOn: new Date().toISOString(), 51 | appId: "NaderDabitLensStarter", 52 | ...updatedProfile, 53 | } 54 | 55 | if (profile.coverPicture) { 56 | newMeta.cover_picture = profile.coverPicture.original.url 57 | } else { 58 | newMeta.cover_picture = null 59 | } 60 | 61 | const added = await client.add(JSON.stringify(newMeta)) 62 | 63 | const newMetadataURI = `https://ipfs.infura.io/ipfs/${added.path}` 64 | 65 | // using the GraphQL API may be unnecessary 66 | // if you are not using gasless transactions 67 | const urqlClient = await createClient() 68 | const data = await urqlClient.mutation(createProfileMetadataTypedData, { 69 | profileId: profile.id, metadata: newMetadataURI 70 | }).toPromise() 71 | 72 | const typedData = data.data.createSetProfileMetadataTypedData.typedData; 73 | 74 | const signature = await signedTypeData(typedData.domain, typedData.types, typedData.value) 75 | const { v, r, s } = splitSignature(signature); 76 | 77 | const contract = new ethers.Contract( 78 | PERIPHERY_CONTRACT_ADDRESS, 79 | PERIPHERY, 80 | getSigner() 81 | ) 82 | 83 | const tx = await contract.setProfileMetadataURIWithSig({ 84 | profileId: profile.id, 85 | metadata: newMetadataURI, 86 | sig: { 87 | v, 88 | r, 89 | s, 90 | deadline: typedData.value.deadline, 91 | }, 92 | }); 93 | console.log('tx: ', tx) 94 | } 95 | 96 | if (!profile) { 97 | return null 98 | } 99 | 100 | const meta = profile.attributes.reduce((acc, next) => { 101 | acc[next.key] = next.value 102 | return acc 103 | }, {}) 104 | 105 | return ( 106 |
107 |

Edit Profile

108 |
109 | 110 | setUpdatedProfile({ ...updatedProfile, name: e.target.value })} 114 | /> 115 | 116 |