├── .env.development ├── .env.production ├── .eslintrc.json ├── .gitignore ├── README.md ├── cadence └── contracts │ └── Profile.cdc ├── components ├── Landing.js ├── Profile.js └── Transaction.js ├── contexts ├── AuthContext.js └── TransactionContext.js ├── flow.json ├── flow └── config.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── about.js ├── api │ └── hello.js └── index.js ├── public ├── favicon.ico ├── favicon.png ├── flow-logo.svg └── vercel.svg └── styles ├── Home.module.css └── globals.css /.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ACCESS_NODE_API=http://localhost:8080 2 | NEXT_PUBLIC_DISCOVERY_WALLET=http://localhost:8701/fcl/authn 3 | NEXT_PUBLIC_CONTRACT_PROFILE=0xf8d6e0586b0a20c7 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ACCESS_NODE_API=https://rest-testnet.onflow.org 2 | NEXT_PUBLIC_DISCOVERY_WALLET=https://fcl-discovery.onflow.org/testnet/authn 3 | NEXT_PUBLIC_CONTRACT_PROFILE=0xba1132bc08f82fe2 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/no-unknown-property": ["error", { "ignore": ["indeterminate"] }] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | package-lock.json 37 | 38 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to use the Flow Client Library (FCL) with Next.js 2 | 3 | Everything you need to build a Next.js project with the Flow Client Library (FCL). 4 | 5 | For a SvelteKit example, see my other repo: https://github.com/muttoni/fcl-sveltekit 6 | 7 | ## [Live demo](https://fcl-nextjs-quickstart.vercel.app/) 8 | 9 | [![image](https://user-images.githubusercontent.com/27052451/146340356-e34f3c47-43bc-4c11-926b-b82b99d561c6.png)](https://fcl-sveltekit.vercel.app/) 10 | 11 | ## Running on Flow Testnet 12 | This project will run on the Flow testnet simply as: 13 | ```bash 14 | npm run build 15 | npm run start 16 | ``` 17 | 18 | ## Developing with Flow emulator 19 | 20 | **Pre-Requisite**: To develop locally, make sure you have the Flow CLI installed: https://docs.onflow.org/flow-cli/install/ 21 | 22 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start the emulator, deploy the contracts, followed by the development server: 23 | 24 | ```bash 25 | flow emulator start --dev-wallet 26 | flow project deploy --network emulator 27 | 28 | npm run dev 29 | # or start the server and open the app in a new browser tab 30 | npm run dev -- --open 31 | ``` 32 | 33 | > NOTE: If you are switching between testnet and the emulator without changing tabs, FCL will keep you logged in with your testnet address (or vice-versa). Remember to logout inbetween environments to avoid runtime errors! 34 | 35 | ## Building 36 | 37 | Before creating a production version of your app, build it! 38 | 39 | ```bash 40 | npm run build 41 | ``` 42 | 43 | ## Testimonials 44 | 45 | Screenshot 2022-04-03 at 12 12 20 46 | -------------------------------------------------------------------------------- /cadence/contracts/Profile.cdc: -------------------------------------------------------------------------------- 1 | /** Generic Profile Contract 2 | 3 | License: MIT 4 | 5 | I am trying to figure out a generic re-usable Profile Micro-Contract 6 | that any application can consume and use. It should be easy to integrate 7 | this contract with any application, and as a user moves from application 8 | to application this profile can come with them. A core concept here is 9 | given a Flow Address, a profiles details can be publically known. This 10 | should mean that if an application were to use/store the Flow address of 11 | a user, than this profile could be visible, and maintained with out storing 12 | a copy in an applications own databases. I believe that anytime we can move 13 | a common database table into a publically accessible contract/resource is a 14 | win. 15 | 16 | could be a little more than that too. As Flow Accounts can now have 17 | multiple contracts, it could be fun to allow for these accounts to have 18 | some basic information too. https://flow-view-source.com is a side project 19 | of mine (qvvg) and if you are looking at an account on there, or a contract 20 | deployed to an account I will make it so it pulls info from a properly 21 | configured Profile Resource. 22 | 23 | ==================== 24 | ## Table of Contents 25 | ==================== 26 | Line 27 | Intro ......................................................... 1 28 | Table of Contents ............................................. 27 29 | General Profile Contract Info ................................. 41 30 | Examples ...................................................... 50 31 | Initializing a Profile Resource ............................. 59 32 | Interacting with Profile Resource (as Owner) ................ 112 33 | Reading a Profile Given a Flow Address ...................... 160 34 | Reading a Multiple Profiles Given Multiple Flow Addresses ... 192 35 | Checking if Flow Account is Initialized ..................... 225 36 | 37 | 38 | ================================ 39 | ## General Profile Contract Info 40 | ================================ 41 | 42 | Currently a profile consists of a couple main pieces: 43 | - name – An alias the profile owner would like to be refered as. 44 | - avatar - An href the profile owner would like applications to use to represent them graphically. 45 | - color - A valid html color (not verified in any way) applications can use to accent and personalize the experience. 46 | - info - A short description about the account. 47 | 48 | =========== 49 | ## Examples 50 | =========== 51 | 52 | The following examples will include both raw cadence transactions and scripts 53 | as well as how you can call them from FCL. The FCL examples are currently assuming 54 | the following configuration is called somewhere in your application before the 55 | the actual calls to the chain are invoked. 56 | 57 | ================================== 58 | ## Initializing a Profile Resource 59 | ================================== 60 | 61 | Initializing should be done using the paths that the contract exposes. 62 | This will lead to predictability in how applications can look up the data. 63 | 64 | ----------- 65 | ### Cadence 66 | ----------- 67 | 68 | import Profile from 0xba1132bc08f82fe2 69 | 70 | transaction { 71 | let address: address 72 | prepare(currentUser: AuthAccount) { 73 | self.address = currentUser.address 74 | if !Profile.check(self.address) { 75 | currentUser.save(<- Profile.new(), to: Profile.privatePath) 76 | currentUser.link<&Profile.Base{Profile.Public}>(Profile.publicPath, target: Profile.privatePath) 77 | } 78 | } 79 | post { 80 | Profile.check(self.address): "Account was not initialized" 81 | } 82 | } 83 | 84 | ------- 85 | ### FCL 86 | ------- 87 | 88 | import {query} from "@onflow/fcl" 89 | 90 | await mutate({ 91 | cadence: ` 92 | import Profile from 0xba1132bc08f82fe2 93 | 94 | transaction { 95 | prepare(currentUser: AuthAccount) { 96 | self.address = currentUser.address 97 | if !Profile.check(self.address) { 98 | currentUser.save(<- Profile.new(), to: Profile.privatePath) 99 | currentUser.link<&Profile.Base{Profile.Public}>(Profile.publicPath, target: Profile.privatePath) 100 | } 101 | } 102 | post { 103 | Profile.check(self.address): "Account was not initialized" 104 | } 105 | } 106 | `, 107 | limit: 55, 108 | }) 109 | 110 | =============================================== 111 | ## Interacting with Profile Resource (as Owner) 112 | =============================================== 113 | 114 | As the owner of a resource you can update the following: 115 | - name using `.setName("MyNewName")` (as long as you arent verified) 116 | - avatar using `.setAvatar("https://url.to.my.avatar")` 117 | - color using `.setColor("tomato")` 118 | - info using `.setInfo("I like to make things with Flow :wave:")` 119 | 120 | ----------- 121 | ### Cadence 122 | ----------- 123 | 124 | import Profile from 0xba1132bc08f82fe2 125 | 126 | transaction(name: String) { 127 | prepare(currentUser: AuthAccount) { 128 | currentUser 129 | .borrow<&{Profile.Owner}>(from: Profile.privatePath)! 130 | .setName(name) 131 | } 132 | } 133 | 134 | ------- 135 | ### FCL 136 | ------- 137 | 138 | import {mutate} from "@onflow/fcl" 139 | 140 | await mutate({ 141 | cadence: ` 142 | import Profile from 0xba1132bc08f82fe2 143 | 144 | transaction(name: String) { 145 | prepare(currentUser: AuthAccount) { 146 | currentUser 147 | .borrow<&{Profile.Owner}>(from: Profile.privatePath)! 148 | .setName(name) 149 | } 150 | } 151 | `, 152 | args: (arg, t) => [ 153 | arg("qvvg", t.String), 154 | ], 155 | limit: 55, 156 | }) 157 | 158 | ========================================= 159 | ## Reading a Profile Given a Flow Address 160 | ========================================= 161 | 162 | ----------- 163 | ### Cadence 164 | ----------- 165 | 166 | import Profile from 0xba1132bc08f82fe2 167 | 168 | pub fun main(address: Address): Profile.ReadOnly? { 169 | return Profile.read(address) 170 | } 171 | 172 | ------- 173 | ### FCL 174 | ------- 175 | 176 | import {query} from "@onflow/fcl" 177 | 178 | await query({ 179 | cadence: ` 180 | import Profile from 0xba1132bc08f82fe2 181 | 182 | pub fun main(address: Address): Profile.ReadOnly? { 183 | return Profile.read(address) 184 | } 185 | `, 186 | args: (arg, t) => [ 187 | arg("0xba1132bc08f82fe2", t.Address) 188 | ] 189 | }) 190 | 191 | ============================================================ 192 | ## Reading a Multiple Profiles Given Multiple Flow Addresses 193 | ============================================================ 194 | 195 | ----------- 196 | ### Cadence 197 | ----------- 198 | 199 | import Profile from 0xba1132bc08f82fe2 200 | 201 | pub fun main(addresses: [Address]): {Address: Profile.ReadOnly} { 202 | return Profile.readMultiple(addresses) 203 | } 204 | 205 | ------- 206 | ### FCL 207 | ------- 208 | 209 | import {query} from "@onflow/fcl" 210 | 211 | await query({ 212 | cadence: ` 213 | import Profile from 0xba1132bc08f82fe2 214 | 215 | pub fun main(addresses: [Address]): {Address: Profile.ReadOnly} { 216 | return Profile.readMultiple(addresses) 217 | } 218 | `, 219 | args: (arg, t) => [ 220 | arg(["0xba1132bc08f82fe2", "0xf76a4c54f0f75ce4", "0xf117a8efa34ffd58"], t.Array(t.Address)), 221 | ] 222 | }) 223 | 224 | ========================================== 225 | ## Checking if Flow Account is Initialized 226 | ========================================== 227 | 228 | ----------- 229 | ### Cadence 230 | ----------- 231 | 232 | import Profile from 0xba1132bc08f82fe2 233 | 234 | pub fun main(address: Address): Bool { 235 | return Profile.check(address) 236 | } 237 | 238 | ------- 239 | ### FCL 240 | ------- 241 | 242 | import {query} from "@onflow/fcl" 243 | 244 | await query({ 245 | cadence: ` 246 | import Profile from 0xba1132bc08f82fe2 247 | 248 | pub fun main(address: Address): Bool { 249 | return Profile.check(address) 250 | } 251 | `, 252 | args: (arg, t) => [ 253 | arg("0xba1132bc08f82fe2", t.Address) 254 | ] 255 | }) 256 | 257 | */ 258 | pub contract Profile { 259 | pub let publicPath: PublicPath 260 | pub let privatePath: StoragePath 261 | 262 | pub resource interface Public { 263 | pub fun getName(): String 264 | pub fun getAvatar(): String 265 | pub fun getColor(): String 266 | pub fun getInfo(): String 267 | pub fun asReadOnly(): Profile.ReadOnly 268 | } 269 | 270 | pub resource interface Owner { 271 | pub fun getName(): String 272 | pub fun getAvatar(): String 273 | pub fun getColor(): String 274 | pub fun getInfo(): String 275 | 276 | pub fun setName(_ name: String) { 277 | pre { 278 | name.length <= 15: "Names must be under 15 characters long." 279 | } 280 | } 281 | pub fun setAvatar(_ src: String) 282 | pub fun setColor(_ color: String) 283 | pub fun setInfo(_ info: String) { 284 | pre { 285 | info.length <= 280: "Profile Info can at max be 280 characters long." 286 | } 287 | } 288 | } 289 | 290 | pub resource Base: Owner, Public { 291 | access(self) var name: String 292 | access(self) var avatar: String 293 | access(self) var color: String 294 | access(self) var info: String 295 | 296 | init() { 297 | self.name = "Anon" 298 | self.avatar = "" 299 | self.color = "#232323" 300 | self.info = "" 301 | } 302 | 303 | pub fun getName(): String { return self.name } 304 | pub fun getAvatar(): String { return self.avatar } 305 | pub fun getColor(): String {return self.color } 306 | pub fun getInfo(): String { return self.info } 307 | 308 | pub fun setName(_ name: String) { self.name = name } 309 | pub fun setAvatar(_ src: String) { self.avatar = src } 310 | pub fun setColor(_ color: String) { self.color = color } 311 | pub fun setInfo(_ info: String) { self.info = info } 312 | 313 | pub fun asReadOnly(): Profile.ReadOnly { 314 | return Profile.ReadOnly( 315 | address: self.owner?.address, 316 | name: self.getName(), 317 | avatar: self.getAvatar(), 318 | color: self.getColor(), 319 | info: self.getInfo() 320 | ) 321 | } 322 | } 323 | 324 | pub struct ReadOnly { 325 | pub let address: Address? 326 | pub let name: String 327 | pub let avatar: String 328 | pub let color: String 329 | pub let info: String 330 | 331 | init(address: Address?, name: String, avatar: String, color: String, info: String) { 332 | self.address = address 333 | self.name = name 334 | self.avatar = avatar 335 | self.color = color 336 | self.info = info 337 | } 338 | } 339 | 340 | pub fun new(): @Profile.Base { 341 | return <- create Base() 342 | } 343 | 344 | pub fun check(_ address: Address): Bool { 345 | return getAccount(address) 346 | .getCapability<&{Profile.Public}>(Profile.publicPath) 347 | .check() 348 | } 349 | 350 | pub fun fetch(_ address: Address): &{Profile.Public} { 351 | return getAccount(address) 352 | .getCapability<&{Profile.Public}>(Profile.publicPath) 353 | .borrow()! 354 | } 355 | 356 | pub fun read(_ address: Address): Profile.ReadOnly? { 357 | if let profile = getAccount(address).getCapability<&{Profile.Public}>(Profile.publicPath).borrow() { 358 | return profile.asReadOnly() 359 | } else { 360 | return nil 361 | } 362 | } 363 | 364 | pub fun readMultiple(_ addresses: [Address]): {Address: Profile.ReadOnly} { 365 | let profiles: {Address: Profile.ReadOnly} = {} 366 | for address in addresses { 367 | let profile = Profile.read(address) 368 | if profile != nil { 369 | profiles[address] = profile! 370 | } 371 | } 372 | return profiles 373 | } 374 | 375 | 376 | init() { 377 | self.publicPath = /public/profile 378 | self.privatePath = /storage/profile 379 | 380 | self.account.save(<- self.new(), to: self.privatePath) 381 | self.account.link<&Base{Public}>(self.publicPath, target: self.privatePath) 382 | 383 | self.account 384 | .borrow<&Base{Owner}>(from: self.privatePath)! 385 | .setName("qvvg") 386 | } 387 | } -------------------------------------------------------------------------------- /components/Landing.js: -------------------------------------------------------------------------------- 1 | import "../flow/config"; 2 | import { useAuth } from "../contexts/AuthContext"; 3 | import Profile from "./Profile"; 4 | import Link from 'next/link'; 5 | 6 | function Landing() { 7 | const { currentUser, profileExists, logOut, logIn, signUp, createProfile } = 8 | useAuth(); 9 | 10 | const AuthedState = () => { 11 | return ( 12 |
13 |
Logged in as: {currentUser?.addr ?? "No Address"}
14 | 15 | 16 |

Controls

17 | 18 |
19 | ); 20 | }; 21 | 22 | const UnauthenticatedState = () => { 23 | return ( 24 |
25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | const Messages = () => { 32 | if (!currentUser?.loggedIn) { 33 | return "Get started by logging in or signing up."; 34 | } else { 35 | if (profileExists) { 36 | return "Your Profile lives on the blockchain."; 37 | } else { 38 | return "Create a profile on the blockchain."; 39 | } 40 | } 41 | }; 42 | 43 | return ( 44 |
45 |
46 |
47 |

48 | Welcome to Web3 49 |

50 |

51 | 52 |

53 | {profileExists && } 54 |
55 |
56 | {currentUser?.loggedIn ? : } 57 |
58 |
59 |
60 | ); 61 | } 62 | 63 | export default Landing; 64 | -------------------------------------------------------------------------------- /components/Profile.js: -------------------------------------------------------------------------------- 1 | import { useAuth } from "../contexts/AuthContext"; 2 | import { useState } from "react"; 3 | 4 | function Profile() { 5 | const { userProfile, updateProfile } = useAuth(); 6 | const [editedProfile, setProfile] = useState(userProfile); 7 | 8 | const saveProfile = () => { 9 | updateProfile(editedProfile); 10 | }; 11 | 12 | return ( 13 |
14 | 25 |
26 | 39 | 40 | 52 |
53 | 54 | 55 |