MAINNET:
Loading...
TESTNET:
Loading...
/
onflow.org
Flow Playground

Flow App Quickstart


Last Updated: March 20th 2021

Follow this guide to understand the basics of how to build an app that interacts with the Flow blockchain using @onflow/fcl

This guide uses create-react-app and does not require any server-side code. @onflow/fcl is not tied to any front-end framework.

In this quickstart you will:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile if its there.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

Let's begin building a decentralized application.

Create React App and Other Dependencies

Run the following commands to initialize a new FCL project.

yarn create react-app my-app
cd my-app
yarn add @onflow/fcl @onflow/types

Those dependencies:

  • @onflow/fcl is the latest build of FCL.
  • @onflow/types is a conversion layer between JavaScript and Cadence (Flow's native language). These are used when we want to pass JavaScript into Cadence transactions and scripts.

Configuration

Generally it's a good idea to use environment variables for our configuration. This will allow us to change environments easily and Create React App comes with fairly good support for them out of the box. We will then need to pass these environment variables to FCL before it talks to anything else. To achieve this we will create two files (./.env.local, ./src/config.js) one to hold our env. variables locally, one to import those env. variables and supply them to FCL. Then we will import our FCL configuration as the very first thing in our application.

Note: Create React App requires all environment variables to be prefixed with REACT_APP_*

touch .env.local        # Create a .env.local file to store our environment variables
touch ./src/config.js   # Create a ./src/config.js file where we will import our environment variables and configure FCL

Now that we have our files we should add in our environment variables. Open .env.local and add the following to it.

# File: .env.local

# ACCESS_NODE will be the endpoint our application
# will use to talk to the Flow blockchain.
REACT_APP_ACCESS_NODE=https://access-testnet.onflow.org

# WALLET_DISCOVERY will be the endpoint our application
# will use to discover available FCL compatible wallets.
REACT_APP_WALLET_DISCOVERY=https://fcl-discovery.onflow.org/testnet/authn

# CONTRACT_PROFILE will be the address that has the Profile
# smart contract we will be using in this guide.
REACT_APP_CONTRACT_PROFILE=0xba1132bc08f82fe2

These environment variables should now be available for us to use when we configure FCL, which we will do next. Open ./src/config.js and add the following to it.

// File: ./src/config.js

import {config} from "@onflow/fcl"

config()
  .put("accessNode.api", process.env.REACT_APP_ACCESS_NODE) // Configure FCL's Access Node
  .put("challenge.handshake", process.env.REACT_APP_WALLET_DISCOVERY) // Configure FCL's Wallet Discovery mechanism
  .put("0xProfile", process.env.REACT_APP_CONTRACT_PROFILE) // Will let us use `0xProfile` in our Cadence

We now have a file that configures FCL but... it is not yet being invoked, so FCL still remains unconfigured in our application. The final step of this section is to import this file as the first thing in our application. Open ./src/index.js and add the following as the first line:

// File: ./src/index.js

import "./config" // Imports environment variables and configures FCL
// Then the rest of ./src/index.js
import React from "react"
import ReactDOM from "react-dom"

:tada: Congrats!! You have configured FCL, which is the very first step in having a decentralized application built on Flow (Testnet).

We should now be able to check off the following:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

Authentication

Believe it or not, our application already has authentication. We got it when we configured the challenge.handshake value. That one piece of configuration tells FCL everything it needs to know in order to authenticate users on Flow (Testnet) using FCL compatible wallets.

Let's learn how to interact with our current user by learning about the following:

  • How to Sign Up
  • How to Log In
  • How to Log Out
  • Subscribe to the Current User's Info

We will learn about them by building a React component that will show the user's Flow Address and a Log Out button when they are authenticated, but when they are unauthenticated it will show Sign Up and Log In buttons.

We are going to call this component AuthCluster and it will live at /src/auth-cluster.js. Here is its code, we will talk about what's going on in it afterwords.

// File: ./src/auth-cluster.js

import React, {useState, useEffect} from "react"
import * as fcl from "@onflow/fcl"

export function AuthCluster() {
  const [user, setUser] = useState({loggedIn: null})
  useEffect(() => fcl.currentUser().subscribe(setUser), [])

  if (user.loggedIn) {
    return (
      <div>
        <span>{user?.addr ?? "No Address"}</span>
        <button onClick={fcl.unauthenticate}>Log Out</button>
      </div>
    )
  } else {
    return (
      <div>
        <button onClick={fcl.logIn}>Log In</button>
        <button onClick={fcl.signUp}>Sign Up</button>
      </div>
    )
  }
}

There is a lot going on in there, we should take a closer look, in particular at the following:

  • fcl.currentUser().subscribe(setUser)
  • fcl.unauthenticate
  • fcl.logIn and fcl.signUp

fcl.currentUser().subscribe(setUser)

Internally and conceptually FCL sort of uses the Actor Model (as much as anything in JavaScript really can...), this allows us to reactively subscribe to things, like in this instance, when the current user changes from being unauthenticated to authenticated. fcl.currentUser() returns us the Current User Actor, for the purposes of this guide we can think of it this way: fcl.currentUser() is going to return an object that has all the things we can do with a current user. One of the things we can do with a current user is subscribe to its state, passing it a callback function. This means that anytime the state of the current user changes, our callback will be invoked with the current user's current state. This callback is invoked immediately as soon as we subscribe, but due to how React's useState and useEffect hooks work we need to be sure to give ourselves some default values.

fcl.unauthenticate()

fcl.unauthenticate() is a function that is an alias to fcl.currentUser().unauthenticate(). It will trigger the unauthenticate sequence inside of the Current User Actor.

fcl.logIn() and fcl.signUp()

fcl.logIn() and fcl.signUp() are function that are currently both alias to fcl.currentUser().authenticate(). It will trigger the authenticate sequence inside of the Current User Actor. FCL, by design, considers both of these operations the same. We will be expanding on this aspect of FCL in the future, and future versions will be able to pass on the intent "User wants to Log In" and "User wants to Sign Up" onto the wallets, so using the respective aliases now will lead to gaining this future intent later.

Using our AuthCluster

We should then import and add our AuthCluster component to our application in ./src/App.js. I've replaced ./src/App.js to look like the following:

// File: ./src/App.js

import React from "react"
import {AuthCluster} from "./auth-cluster"

export default function App() {
  return (
    <div>
      <AuthCluster />
    </div>
  )
}

:tada: Congrats!! Users of our application can now authenticate and unauthenticate.

We should now be able to check off the following:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

Flow Smart Contracts

I've started to think of Flow smart contracts as stateful modules, and have been trying to break them down into a concept which I have been calling Micro Contracts. To me a Micro Contract is a smaller self contained smart contract that does a single thing real well, and should be re-usable across various applications with out an additional instance of it being deployed. As our Flow ecosystem grows and evolves hopefully the more of these building blocks become available, allowing you as the app developer to build on the shoulders of giants, enabling you to focus on the logic that makes your applications and smart contracts special.

The rest of this guide is going to focus on a Profile smart contract that you can find at: https://flow-view-source.com/testnet/account/0xba1132bc08f82fe2/contract/Profile

The idea behind that Profile smart contract is that a user can set their public profile once and bring it with them from application to application. What this means for you as an app developer is that your application can use this functionality. Not only that but using it should be easy enough to use in this quick start guide.

Now there is something very important that you as an application developer need to keep in mind when developing on Flow. Generally speaking, Flow accounts need to have a resource from a smart contract in their storage, in order to interact with said contract. We don't get to silently slip this resource into their storage, the user needs to actively authorize this action. This resource is usually where we store the account's specific data (that stateful part of stateful modules). It also generally acts as the interface into the contract itself. If we were to use the Profile smart contract as an example, the resource the Flow account needs to have acts as the storage where we keep the Profile's details, and it exposes functionality for the owner of the resource to update those details.

It's often not enough to only have the resource, we often need a publicly accessible interface too. This publicly accessible interface in Cadence terms is called a capability, and is usually a sub-set of what the resource is able to do. In the case of the Profile smart contract, this public capability is what allows our application to read the profile's information without the Flow account authorizing our access ever time, meaning we don't ever need to store a copy of their info.

We usually call it an "initialization" when setting up a Flow account with a smart contract's resources and additional capabilities. As a Flow application developer you will need to think about this sort of thing a lot, and figure out what this means for your application's UX.

:tada: Congrats!! There is so much more to learn about the above, but for now the important bit is to remember that your users need to authorize the initialization of resources from your smart contracts into their Flow accounts.

We should now be able to check off the following:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

FCL and Scripts

In FCL, I almost always start off writing a script like this: fcl.send([]).then(fcl.decode). fcl.send() takes an array that lets us describe what we want our script to be, and fcl.decode is going to parse out the return value from our script and turn it into JavaScript values.

Our first script isn't going to have anything to do with our Profile smart contract, its going to use Cadence, to add two numbers. We are going to hardcode in these numbers to start with, and then pass them in as arguments. Let's look at our first script.

import * as fcl from "@onflow/fcl"

await fcl
  .send([
    fcl.script`
      pub fun main(): Int {
        return 5 + 4
      }
    `,
  ])
  .then(fcl.decode)

In Cadence scripts, pub fun main() is what gets invoked, in this case it is returning an integer (Int). fcl.send sends our script to the blockchain (via our configured access node) and then fcl.decode is decoding the response, which should return 9 in this case.

The above snippet is fine as a starting point, but we can do so much more here, like replacing those magic numbers with script arguments.

import * as fcl from "@onflow/fcl"
import * as t from "@onflow/types"

await fcl
  .send([
    fcl.script`
      pub fun main(a: Int, b: Int): Int {
        return a + b
      }
    `,
    fcl.args([
      fcl.arg(5, t.Int), // a
      fcl.arg(4, t.Int), // b
    ]),
  ])
  .then(fcl.decode)

In the above we have changed pub fun main(): Int to pub fun main(a: Int, b: Int): Int, this tells Cadence to expect two arguments, in this case both are integers. fcl.args([]) allows us to pass in arguments to our Cadence script. The order of arguments matters here. The first argument in the array is going to be the first argument to our main function. We then see two fcl.arg(value, t.Int) calls passed to fcl.args([]), the first will be our a argument, and the second will be our b argument. That t.Int needs to match the argument's type declaration in Cadence, as there is a corresponding t.* for every Cadence Type.

TIP: If you want to try this out, flow-view-source exposes both fcl and t in your browser's JavaScript console. Try pasting and executing the above (sans-import) snippet there.

I just so happen to know that the Rawr (Testnet) at 0xba1132bc08f82fe2 smart contract has this add functionality that we are currently using. Let's import the Rawr smart contract and use its add function instead of writing out our bespoke implementation.

import * as fcl from "@onflow/fcl"
import * as t from "@onflow/types"

await fcl
  .send([
    fcl.script`
      import Rawr from 0xba1132bc08f82fe2

      pub fun main(a: Int, b: Int): Int {
        return Rawr.add(a: a, b: b)
      }
    `,
    fcl.args([
      fcl.arg(5, t.Int), // a
      fcl.arg(4, t.Int), // b
    ]),
  ])
  .then(fcl.decode)

So at this point we should be able to import smart contracts and execute scripts against them. Let's apply this new found knowledge against our Profile smart contract. I know for a fact that 0xba1132bc08f82fe2 (which also happens to be where the Profile smart contract is deployed) has a profile, so let's see if we can query the blockchain for it.

import * as fcl from "@onflow/fcl"
import * as t from "@onflow/types"

await fcl
  .send([
    fcl.script`
      import Profile from 0xba1132bc08f82fe2

      pub fun main(address: Address): Profile.ReadOnly? {
        return Profile.read(address)
      }
    `,
    fcl.args([
      fcl.arg("0xba1132bc08f82fe2", t.Address), // <-- t.Address this time :)
    ]),
  ])
  .then(fcl.decode)

We should get back something like this:

{
  "address": "0xba1132bc08f82fe2",
  "name": "qvvg",
  "avatar": "https://i.imgur.com/r23Zhvu.png",
  "color": "#6600ff",
  "info": "Flow Core Team. Creator and Maintainer of FCL and the flow-js-sdk.",
  "verified": true
}

A lot of work has been done in the Profile smart contract, to make interacting with it from a web app and FCL a nicer experience. I would highly recommend having a look through it, there are a lot of small lessons in there.

:tada: Congrats!! You just used rules that were defined in a smart contract to query a Flow Account for a profile on the Flow blockchain.

Checking if the Flow Account is Initialized with the Profile Contract

Not all Flow accounts are going to have a profile though, if you are new to Flow your account right now probably doesn't have one, so being able to check if an account is initialized and then being able initialize an account is extremely important. That is what we will cover next.

Let's start by creating a directory for our Flow Scripts and Transactions. I generally like to call this directory flow, name my scripts with *.script.js and name my transactions with *.tx.js. Our first script is going to check if an account specified by a supplied address is initialized with a profile.

mkdir ./src/flow
touch ./src/flow/is-initialized.script.js

The Profile smart contract exposes a helper function that lets us check if an address is initialized Profile.check(address), it returns a boolean. Let's use that in our new ./src/flow/is-initialized.script.js file.

// File: ./src/flow/is-initialized.script.js

import * as fcl from "@onflow/fcl"
import * as t from "@onflow/types"

export async function isInitialized(address) {
  if (address == null)
    throw new Error("isInitialized(address) -- address required")

  return fcl
    .send([
      fcl.script`
        import Profile from 0xProfile

        pub fun main(address: Address): Bool {
          return Profile.check(address)
        }
      `,
      fcl.args([fcl.arg(address, t.Address)]),
    ])
    .then(fcl.decode)
}

Something new was introduced in the above file, we used 0xProfile instead of 0xba1132bc08f82fe2 in the import statement. Way way way back at the start of this guide we added in the configuration config().put("0xProfile", "0xba1132bc08f82fe2"), it turns out that when we are using FCL, that configuration allows us to pull the corresponding addresses from any configuration value where its key starts with 0x. This is super convenient when you move from one environment/chain to another, as all you should need to do is update your environment variables to reflect your new environment/blockchain. None of your code should need to change.

Other than that newly introduced import, hopefully the above is looking familiar to you now. Let's put this aside for now and move onto how to initialize an account with a Profile.

:tada: Congrats!! You now have a re-usable script that you can use to check if an account is initialized.

We should now be able to check off the following:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

Initializing an Account with a Profile

Initializing an Account with a Profile is going to require a transaction. Transactions are very similar to scripts, for example: you supply them with some Cadence, they accept arguments. But there is a little more to them... You need a transaction when you want to permanently change the state of the Flow blockchain, there is a cost (in FLOW) involved with this (often covered by FCL compatible wallets), a Flow account needs to propose the change (acts as a nonce), and if you need the permission of the owner of the state you are changing. Those three aspects we call roles and are as follows, the payer, the proposer, and authorizers (one for each owner of state). The most common intent will be that the Current User is responsible for all three roles, and by extension the wallet. What we need is the Current User to Authorize its participation as all three of the roles (payer, proposer, authorizer). Luckily for us FCL makes this pretty easy, fcl.currentUser() has an authorization function that can be used to do just that. You will need fcl.currentUser().authorization in almost every standard transaction, usually multiple times, because of this we have aliased it to fcl.authz.

Transactions also require a computation limit. This value will eventually be tied to the cost to the payer of the transaction, and it's in your best interest to keep it as low as possible.

Just as with the script, a good place to start with transactions is our good old friend fcl.send([]).then(fcl.decode). In the case of a transaction the decode is going to supply us the transaction's id (or an error) that we can then use to query the status of the transaction.

Let's create a ./src/flow/init-account.tx.js file with the following FCL transaction that initializes an account.

// File: ./src/flow/init-account.tx.js

import * as fcl from "@onflow/fcl"
import * as t from "@onflow/types"

export async function initAccount() {
  const txId = await fcl
    .send([
      // Transactions use fcl.transaction instead of fcl.script
      // Their syntax is a little different too
      fcl.transaction`
        import Profile from 0xProfile

        transaction {
          // We want the account's address for later so we can verify if the account was initialized properly
          let address: Address

          prepare(account: AuthAccount) {
            // save the address for the post check
            self.address = account.address

            // Only initialize the account if it hasn't already been initialized
            if (!Profile.check(self.address)) {
              // This creates and stores the profile in the user's account
              account.save(<- Profile.new(), to: Profile.privatePath)

              // This creates the public capability that lets applications read the profile's info
              account.link<&Profile.Base{Profile.Public}>(Profile.publicPath, target: Profile.privatePath)
            }
          }

          // verify that the account has been initialized
          post {
            Profile.check(self.address): "Account was not initialized"
          }
        }
      `,
      fcl.payer(fcl.authz), // current user is responsible for paying for the transaction
      fcl.proposer(fcl.authz), // current user acting as the nonce
      fcl.authorizations([fcl.authz]), // current user will be first AuthAccount
      fcl.limit(35), // set the compute limit
    ])
    .then(fcl.decode)

  return fcl.tx(txId).onceSealed()
}

There is a fair bit of stuff going on above, first off that transaction code is rough, no way you can remember that. Eventually you will become fairly familiar with that sort of stuff, but in the mean time if you go look at the smart contract I've documented how to do most of things you can possibly want to do with the Profile smart contract. I forget that sort of stuff all the time, and keeping that documentation with the smart contract is a good way to remind myself how to use the contract, it also should make the contract more accessible to others, like you. I hope that this sort of documentation becomes more of a standard practice in Flow smart contract development.

If we assume that the actual Cadence code was copy-pasted from the smart contract, then there isn't much left here. Let's break it down as follows:

  • fcl.authz -- We have already talked about this above, it's an alias for fcl.currentUser().authorization.
  • fcl.payer(fcl.authz), fcl.proposer(fcl.authz) -- These are saying that the current user is responsible for the payer and proposer roles.
  • fcl.authorizations([fcl.authz]) -- This means that our current user is authorizing the transaction to modify the state of things it owns (storage and public capabilities in this case). We gain access to this permission via the first AuthAccount passed into the prepare statement of our transaction.
  • fcl.tx(txId) -- This is a new actor that we haven't seen before. It keeps track of transaction statuses, in this case for the transaction we just submitted to the blockchain.
  • fcl.tx(txId).onceSealed() -- This is a promise that will resolve once our change is permanently represented by the blockchain. It can also error if something goes wrong.

:tada: Congrats!! We now have a function we can call to initialize the current user's account with a Profile.

We should now be able to check off the following:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

Getting an Account's Profile

We actually saw this already, back when we were talking about scripts. So this section is really going to be about adding it to a file we will call ./src/flow/fetch-profile.script.js

// File: ./src/flow/fetch-profile.script.js

import * as fcl from "@onflow/fcl"
import * as t from "@onflow/types"

export async function fetchProfile(address) {
  if (address == null) return null

  return fcl
    .send([
      fcl.script`
        import Profile from 0xProfile

        pub fun main(address: Address): Profile.ReadOnly? {
          return Profile.read(address)
        }
      `,
      fcl.args([fcl.arg(address, t.Address)]),
    ])
    .then(fcl.decode)
}

:tada: Congrats!! We now have a function we can call to fetch an account's Profile if it has one

We should now be able to check off the following:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

Updating a Profile

Updating a piece of information in a profile is a transaction, the Profile smart contract has an example of it, and explains a bit better what you can do with it (Link for the Lazy), so we wont go into all that much detail here. But you should start to see a pattern with transactions:

  • They always have Cadence.
  • They sometimes have arguments.
  • They always have a payer and proposer.
  • They usually have authorizations.
  • Payer, proposer, and first authorization are more often than not the current user.
  • The more complex the transaction the higher the compute limit.

Initialization transactions are often the most scary looking, while also probably the most similar between smart contracts. Other transactions like the following that will live in ./src/flow/profile-set-name.tx.js will generally borrow a resource from the AuthAccount and do some action on the resource. It is also fairly common to then borrow a public capability from some other account and use something from the borrowed resource on that public capability. An example of this would be sending FLOW tokens to someone else. The AuthAccount will borrow its Vault resource and withdraw (the action) a temporary Vault resource, and the transaction will then borrow the public capability from the recipient of the FLOW tokens that can receive that temporary FLOW token vault. In our case we want to act directly on the borrowed Profile Resource.

This transaction to set the name in the profile is substantially smaller than the initialization transaction we have already seen. Only the owner (AuthAccount) of a resource in storage can borrow it directly, we leverage this fact, and a Profile.Owner interface on the resource to make it so the only one who can set the name on this resource is the owner. Everyone else needs to interact with this resource via the linked public capability which has a limited Profile.Public interface.

Below is our ./src/flow/profile-set-name.tx.js file.

// File: ./src/flow/profile-set-name.tx.js

import * as fcl from "@onflow/fcl"
import * as t from "@onflow/types"

export async function setName(name) {
  const txId = await fcl
    .send([
      fcl.proposer(fcl.authz),
      fcl.payer(fcl.authz),
      fcl.authorizations([fcl.authz]),
      fcl.limit(35),
      fcl.args([fcl.arg(name, t.String)]),
      fcl.transaction`
        import Profile from 0xProfile

        transaction(name: String) {
          prepare(account: AuthAccount) {
            account
              .borrow<&Profile.Base{Profile.Owner}>(from: Profile.privatePath)!
              .setName(name)
          }
        }
      `,
    ])
    .then(fcl.decode)

  return fcl.tx(txId).onceSealed()
}

:tada: Congrats!! We now have a function where we can update the user's name in their Profile.

We should now be able to check off the following:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

Putting it All Together

We now have four files, each with their own functionality in them:

  • ./src/flow/is-initialized.script.js
  • ./src/flow/init-account.tx.js
  • ./src/flow/fetch-profile.scripts.js
  • ./src/flow/profile-set-name.tx.js

These files all interact in various ways with the Flow (Testnet) blockchain. This guide isn't really a guide on React, and everyone has all sorts of opinions on how to deal with state in a React application. Hopefully from those four files, and the ability to subscribe to the current users authentication status, you can see how it could be integrated into your applications and various frameworks of choice. It might help to think of them as powerful predefined API calls, that let you query and mutate the state on blockchain. I have personally found a nice synergy between FCL and Recoil. It has allowed me to limit the number of queries and calls to the blockchain I am making. You can find an example of a more complex application here.

With what we have done above, deployment should work with anything that allows you to set environment variables. I have had pretty good success with hosting applications built like this on IPFS via https://fleek.co and the hash router from react-router-dom. But honestly anything should work.

Mainnet

Eventually you will think your application is ready for Mainnet, and it probably is. The first step will be making sure all of the smart contracts you are using are there. Currently the Flow team needs to approve all the smart contracts that get deployed so we have an idea what's out there, but this requirement will go away in the near future. Second step will be to update all of the environment variables. There will be new values for all 3 of them:

  • REACT_APP_ACCESS_NODE
  • REACT_APP_WALLET_DISCOVERY
  • REACT_APP_CONTRACT_PROFILE

At this point in time, your application should be working on mainnet. YMMV based on how far you stray from only talking to smart contracts via FCL.

I think that checks everything else off of our list:

  • Configure our app so that:
    • @onflow/fcl can talk to Flow (Testnet).
    • @onflow/fcl can talk to FCL Wallet Discovery.
    • It knows about our Profile smart contract.
  • Use @onflow/fcl to authenticate using a Flow (Testnet) Account via a discovered wallet.
  • Talk about resource initialization.
  • Use a script to check if the current account has a profile resource initialized.
  • Use a transaction to initialize a profile.
  • Query an account's profile, if it exists.
  • Use a transaction to update a profile.
  • Next Steps
    • Deployment
    • Path to Flow (Mainnet)

If you have any questions, concerns, comments, or just want to say hi, we would love to have your company in our Discord.

Extra Credit - Adding an Interface That Uses Our Functions

Originally, this guide ended here, but a couple of people on our Discord said they really wanted this guide to include using these functions that we spent so much time making. This next part is a little more opinionated and your mileage may vary, but we are going to continue on by adding some hooks that encapsulate certain ideas and then use those hooks in our view.

In this extra credit section we are going to do the following:

  • Install and setup Recoil for state management.
  • Create a current user hook.
  • Use the current user hook in our AuthCluster component.
  • Create an init hook.
  • Create a InitCluster component.
  • Create a profile hook.
  • Create a ProfileCluster component.

Let's dig into it.

Recoil

Recoil is a state management library for React. It will allow us to have a little more control over when we do certain actions, as well as enable us to encapsulate and isolate the ability to talk to things on the blockchain.

The plan is to have a hook that covers each of the three concepts we currently have (Current User, Initialization, Profile). Each hook will give us access to the corresponding data, as well as expose functions which we can use to interact with the data.

The first thing we need to do with Recoil is install it:

yarn add recoil

Once installed we need to add in the RecoilProvider to the root of our application. I am going to replace ./src/index.js with the following:

// File: ./src/index.js

import "./config"
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import reportWebVitals from "./reportWebVitals"
import {RecoilRoot} from "recoil"

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
)

reportWebVitals()

In the above we are importing the RecoilRoot provider and then nesting our App component in it.

:tada: Congrats!! Recoil should now be installed and setup.

Our Extra Credit checklist is now looking like this:

  • Install and setup Recoil for state management.
  • Create a current user hook.
  • Use the current user hook in our AuthCluster component.
  • Create an init hook.
  • Create a InitCluster component.
  • Create a profile hook.
  • Create a ProfileCluster component.

Current User Hook

For this one I sort of want to start backwards, that is by talking about the outcome I want to see from our hook.

Currently we have an AuthCluster component that lives in ./src/auth-cluster.js, I am wanting to change it so its code looks like this:

// File: ./src/auth-cluster.js

import React, {useState, useEffect} from "react"
import {useCurrentUser} from "./hooks/current-user"

function WithAuth() {
  const cu = useCurrentUser()

  return !cu.loggedIn ? null : (
    <div>
      <span>{cu.addr ?? "No Address"}</span>
      <button onClick={cu.logOut}>Log Out</button>
    </div>
  )
}

function SansAuth() {
  const cu = useCurrentUser()

  return cu.loggedIn ? null : (
    <div>
      <button onClick={cu.logIn}>Log In</button>
      <button onClick={cu.signUp}>Sign Up</button>
    </div>
  )
}

export function AuthCluster() {
  return (
    <>
      <WithAuth />
      <SansAuth />
    </>
  )
}

The difference is nuanced, but the actual consumption of this is on another level to what we had before. The useCurrentUser hook we are going to create hides all the implementation details regarding what we can do with the current user, exposing a standardized set of tools in which we can consume and interact with the current user.

Our WithAuth and SansAuth components are completely self-contained and don't require any additional props at this point. This is a quality I personally try to strive for, as they know when they should render and what they can do when they render, so all that is left is deciding where they should render if they decide they should.

A good next step is creating our new useCurrentUser hook. We will start by creating a ./hooks/ directory and then adding in a ./hooks/current-user.js file for our hook to live in.

mkdir ./src/hooks
touch ./src/hooks/current-user.js

After we have the files our current user hook will look something like this:

// File: ./src/hooks/current-user.js

import {useEffect} from "react"
import {atom, useSetRecoilState, useRecoilValue} from "recoil"
import * as fcl from "@onflow/fcl"

// This is a Recoil atom (https://recoiljs.org/docs/api-reference/core/atom)
// You can think of it as a unique reactive node with our current users state.
// Our hook is going to subscribe to this state.
export const $currentUser = atom({
  key: "CURRENT_USER", // Atoms needs a unique key, we can only ever call the atom function once with this key.
  default: {addr: null, cid: null, loggedIn: null},
})

// We only want a single place where we subscribe and update our
// current user's atom state. That will be this component that we will
// add to the root of our application.
export function CurrentUserSubscription() {
  const setCurrentUser = useSetRecoilState($currentUser)
  useEffect(() => fcl.currentUser().subscribe(setCurrentUser), [setCurrentUser])
  return null
}

// Our actual hook, most of the work is happening
// in our CurrentUserSubscription component so that allows
// this hook to focus on decorating the current user value
// we receive with some helper functions
export function useCurrentUser() {
  const currentUser = useRecoilValue($currentUser)

  return {
    ...currentUser,
    logOut: fcl.unauthenticate,
    logIn: fcl.logIn,
    signUp: fcl.signUp,
  }
}

The last thing we need to do here is to add our CurrentUserSubscription to the root of our application (./src/index.js)

// File: ./src/index.js

import "./config"
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import reportWebVitals from "./reportWebVitals"
import {RecoilRoot} from "recoil"
import {CurrentUserSubscription} from "./hooks/current-user"

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <CurrentUserSubscription />
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
)

reportWebVitals()

:tada: Congrats!! We now have a useCurrentUser hook and we are using it in our AuthCluster component.

Our Extra Credit checklist is now looking like this:

  • Install and setup Recoil for state management.
  • Create a current user hook.
  • Use the current user hook in our AuthCluster component.
  • Create an init hook.
  • Create a InitCluster component.
  • Create a profile hook.
  • Create a ProfileCluster component.

Init Hook

Our next hook will check if an address is initialized with the Profile smart contract, and expose a function that will allow us to initialize the Profile.

I usually like to think about how these hooks will be consumed, so once again I think starting from the component that consumes them. This seems like the right place to start again.

Below is a new component InitCluster, it will live at ./src/init-cluster.js. It will take an address as a prop, and check if it is initialized. If it isn't initialized and the address is for the current user then we should give the current user a button they can press to initialize the account, triggering our init-account transaction. Once the account is initialized the component should reactively reflect that. While we are doing the transaction we should display to the user that the app is doing something.

// File: ./src/init-cluster.js

import {useEffect} from "react"
import {useCurrentUser} from "./hooks/current-user"
import {useInit} from "./hooks/init"

const fmtBool = bool => (bool ? "yes" : "no")

export function InitCluster({address}) {
  const cu = useCurrentUser()
  const init = useInit(address)
  useEffect(() => init.check(), [address])

  if (address == null) return null

  return (
    <div>
      <h3>Init?: {address}</h3>
      <ul>
        <li>
          <strong>Profile: </strong>
          {init.isIdle && <span>{fmtBool(init.profile)}</span>}
          {!init.profile && cu.addr === address && init.isIdle && (
            <button disabled={init.isProcessing} onClick={init.exec}>
              Initialize Profile
            </button>
          )}
          {init.isProcessing && <span>PROCESSING</span>}
        </li>
      </ul>
    </div>
  )
}

The above code introduces our useInit hook, which will live in ./src/hooks/init.js. It has two independent states, one that knows if the profile is initialized and another that knows if our hook is doing some sort of work. We are using atomFamily as there can be more than once instance of an Init Atom (multiple addresses).

First we need to create the file that our new hook will live in:

touch ./src/hooks/init.js

Then we can write our new useInit hook in our new file: ./src/hooks/init.js

// File: ./src/hooks/init.js

import {atomFamily, useRecoilState} from "recoil"
import {isInitialized} from "../flow/is-initialized.script"
import {initAccount} from "../flow/init-account.tx"

const IDLE = "IDLE"
const PROCESSING = "PROCESSING"

// atomFamily is a function that returns a memoized function
// that constructs atoms. This will allow us to define the
// behaviour of the atom once and then construct new atoms
// based on an id (in this case the address)
const $profile = atomFamily({
  key: "INIT::PROFILE::STATE",
  default: null,
})

const $profileStatus = atomFamily({
  key: "INIT::PROFILE::STATUS",
  default: PROCESSING,
})

export function useInit(address) {
  const [profile, setProfile] = useRecoilState($profile(address))
  const [status, setStatus] = useRecoilState($profileStatus(address))

  // check if the supplied address is initialized
  async function check() {
    setStatus(PROCESSING)
    // isInitialized is going to throw an error if the address is null
    // so we will want to avoid that. Because React hooks can't be 
    // dynamically added and removed from a React node, you will find that 
    // this sort of logic will leak into our hooks. We could get around this
    // by changing our isInitialized function to return null instead of 
    // throwing an error.
    if (address != null) await isInitialized(address).then(setProfile)
    setStatus(IDLE)
  }

  // attempt to initialize the current address
  async function exec() {
    setStatus(PROCESSING)
    await initAccount()
    setStatus(IDLE)
    await check()
  }

  return {
    profile,
    check,
    exec,
    isIdle: status === IDLE,
    isProcessing: status === PROCESSING,
    status,
    IDLE,
    PROCESSING,
  }
}

Now that we have an InitCluster component that consumes our new useInit hook, lets add it into our App component. One thing to note here is the use of the current user's address, we are going to use that address for the rest of the guide as it is readily available to us, but any address should work and our component will adapt and only show what it's supposed to show.

// File: ./src/App.js

import React from "react"
import {AuthCluster} from "./auth-cluster"
import {InitCluster} from "./init-cluster"
import {useCurrentUser} from "./hooks/current-user"

export default function App() {
  const cu = useCurrentUser()

  return (
    <div>
      <AuthCluster />
      <InitCluster address={cu.addr} />
    </div>
  )
}

:tada: Congrats!! We now have a useInit hook and an InitCluster component.

Our Extra Credit checklist is now looking like this:

  • Install and setup Recoil for state management.
  • Create a current user hook.
  • Use the current user hook in our AuthCluster component.
  • Create an init hook.
  • Create a InitCluster component.
  • Create a profile hook.
  • Create a ProfileCluster component.

The Profile

Once again let's start with the consumption by creating a ProfileCluster component. It should:

  • Accept an address as a prop.
  • Fetch the profile from the blockchain.
  • Deal with the address not having a profile.
  • If the profile is for the current user, allow the current user to update the name.

The first step is creating the file that will be the home of our new component:

touch ./src/profile-cluster.js

We'll then add the code to the file. This one is a bit more complicated, but is really just more of the same. We consume the hooks and use the functionality that they provide.

// File: ./src/profile-cluster.js

import {useState, useEffect} from "react"
import {useCurrentUser} from "./hooks/current-user"
import {useProfile} from "./hooks/profile"

function ProfileForm() {
  const cu = useCurrentUser()
  const profile = useProfile(cu.addr)
  const [name, setName] = useState("")
  useEffect(() => {
    setName(profile.name)
  }, [profile.name])

  const submit = () => {
    profile.setName(name)
  }

  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      {profile.isIdle && <button onClick={submit}>Update Name</button>}
      {profile.isProcessing && <span>PROCESSING</span>}
    </div>
  )
}

export function ProfileCluster({address}) {
  const profile = useProfile(address)
  useEffect(() => profile.refetch(), [address])
  if (address == null) return null

  return (
    <div>
      <h3>Profile: {address}</h3>
      {profile.isCurrentUser && <ProfileForm />}
      <ul>
        <li>
          <img
            src={profile.avatar}
            width="50px"
            height="50px"
            alt={profile.name}
          />
        </li>
        <li>
          <strong>Name: </strong>
          <span>{profile.name}</span>
          {profile.isCurrentUser && <span> -You</span>}
          {profile.isProcessing && <span>PROCESSING</span>}
        </li>
        <li>
          <strong>Color: </strong>
          <span>{profile.color}</span>
        </li>
        <li>
          <strong>Info: </strong>
          <span>{profile.info}</span>
        </li>
      </ul>
    </div>
  )
}

Then we move onto the hook. useProfile is once again using an atomFamily from Recoil, this is allowing us to have multiple atoms (one per address supplied). It has two pieces of state, one is the status, the other being the profile's details. If an account doesn't have a profile then we are defaulting it to an Anonymous default profile.

// File: ./src/hooks/profile.js

import {atomFamily, useRecoilState} from "recoil"
import {fetchProfile} from "../flow/fetch-profile.script"
import {setName as profileSetName} from "../flow/profile-set-name.tx"
import {useCurrentUser} from "./current-user"

const DEFAULT = {
  name: "Anon",
  color: "#232323",
  info: "...",
  avatar: "https://avatars.onflow.org/avatar/pew",
}
const IDLE = "IDLE"
const PROCESSING = "PROCESSING"

const $profile = atomFamily({
  key: "PROFILE::STATE",
  default: DEFAULT,
})

const $status = atomFamily({
  key: "PROFILE::STATUS",
  default: PROCESSING,
})

export function useProfile(address) {
  const cu = useCurrentUser()
  const [profile, setProfile] = useRecoilState($profile(address))
  const [status, setStatus] = useRecoilState($status(address))

  async function refetch() {
    setStatus(PROCESSING)
    await fetchProfile(address)
      .then(profile => {
        if (profile == null) return profile
        if (profile.avatar === "") profile.avatar = DEFAULT.avatar
        if (profile.info === "") profile.info = DEFAULT.info
        return profile
      })
      .then(setProfile)
    setStatus(IDLE)
  }

  async function setName(name) {
    setStatus(PROCESSING)
    await profileSetName(name)
    setStatus(IDLE)
    await refetch()
  }

  return {
    ...(profile ?? DEFAULT),
    status,
    isCurrentUser: address === cu.addr,
    setName,
    refetch,
    IDLE,
    PROCESSING,
    isIdle: status === IDLE,
    isProcessing: status === PROCESSING,
  }
}

Now that we have a ProfileCluster component and a useProfile hook it's time to use it. Let's update our ./src/App.js file to show the current user's profile, as well as show two additional profiles, one for my account which has a profile, and another for an account that I know does not have a profile.

// File: ./src/App.js

import React from "react"
import {AuthCluster} from "./auth-cluster"
import {InitCluster} from "./init-cluster"
import {ProfileCluster} from "./profile-cluster"
import {useCurrentUser} from "./hooks/current-user"

export default function App() {
  const cu = useCurrentUser()

  return (
    <div>
      <AuthCluster />
      <InitCluster address={cu.addr} />
      <ProfileCluster address={cu.addr} />
      <ProfileCluster address="0xba1132bc08f82fe2" />
      <ProfileCluster address="0xf117a8efa34ffd58" />
    </div>
  )
}

:tada: Congrats!! We now have a useProfile hook and a ProfileCluster component.

Our Extra Credit checklist should now be complete and look like this:

  • Install and setup Recoil for state management.
  • Create a current user hook.
  • Use the current user hook in our AuthCluster component.
  • Create an init hook.
  • Create a InitCluster component.
  • Create a profile hook.
  • Create a ProfileCluster component.

Closing Note on the Extra Credit Section

There are so many additional improvements that can be made in the code we just did. Most of those improvements come down to being smarter about when and how we query the data, pushing them into the category, or how we deal with state. We do (while not very efficient) have a working application that can interact with the Flow (Testnet) blockchain. I am sure there are better/alternative ways of doing this sort of stuff, eventually there always are, so I would recommend using the above as a starting point instead of a gospel. Try and think about the different mechanisms available to you and take the good that aligns with your vision and drop the bad that doesn't.

We are eager to see what you come up with. As mentioned before, if you have any questions, concerns, comments, or just want to say hi, we would love to have your company in our Discord.