How To Build An NFT Minting Dapp On Flow

How To Build An NFT Minting Dapp On Flow

Let's put all the knowledge from my previous articles into practice by writing and deploying a smart contract, build a frontend, and mint some NFTs.

If you’ve followed along with the Flow series so far, you already know that the Flow Blockchain excels in handling digital assets, such as NFTs. It was built from the ground up as a better alternative to Ethereum’s network congestion and high fee issues.

In addition, the Cadence smart contract language is a first-of-its-kind resource-oriented programming language that makes creating and managing digital assets easy and efficient. Although Solidity is excellent at facilitating Web3 through smart contracts, there are drawbacks. Cadence improves upon Solidity’s flaws by providing the ability to upgrade smart contracts and features that reduce the risk of human error, among other improvements.

And finally, the list of tools and libraries available to developers looking to get started is extensive. So let’s put it all together and build something on Flow.

This article is a tutorial about creating a full-fledged NFT-minting dapp for the Flow Blockchain.

Let’s Get To It

For the rest of this article, we will walk through the process of creating an NFT minting dapp on the Flow blockchain.

We will start with setting up and deploying a Cadence smart contract. Then, we will build a front end to connect to our smart contract and mint an NFT into the user’s account.

The functionality we build will allow users to connect their Flow account, create an account if they don’t already have one, then select from one of three images to mint into an NFT. Then, the dapp will display the NFTs from our collection that are in the user’s account. It will be an excellent project to highlight how easy and efficient creating NFTs are on Flow and how effective the Flow Client Library (FCL) is for interacting with the blockchain.

To follow along with this tutorial, you’ll need the following things:

With all of these installed, let’s get started!

1. Set Up Flow Account

Before we start building, we’ll need to set up an account on the Flow blockchain so we can deploy our smart contract. Run the following command to generate a new public and private key pair:

flow keys generate

Be sure to write down the values your console outputs, as we'll need them in the following steps.

Next, we'll head over to the Flow Faucet to create a new address based on our keys and fund our account with some test tokens. Complete the following steps to create your account:

  1. Paste in your public key in the specified input field
  2. Keep the Signature and Hash Algorithms set to default
  3. Complete the captcha
  4. Click on Create Account

image1.png

With a successful account generation, we get a dialogue with our new Flow address containing 1,000 FLOW tokens.

image3.png

Copy the address for use in the next step.

2. Set Up The Smart Contract

Before we build the project frontend, let's create the smart contract we will interact with later.

In the command terminal, navigate to the folder you would like to work from and type the following command to initiate a project:

flow init

This command creates a flow.json file inside the folder, where we'll place all the information we need to deploy our smart contract.

Open the flow.json file in your code editor and we'll set up a testnet account. Inside the accounts section, we'll add a new entry called testnet-account, which contains our new address and the private key generated in the flow keys generate command earlier.

{
    "emulators": {
        "default": {
            "port": 3569,
            "serviceAccount": "emulator-account"
        }
    },
    "contracts": {},
    "networks": {
        "emulator": "127.0.0.1:3569",
        "mainnet": "access.mainnet.nodes.onflow.org:9000",
        "testnet": "access.devnet.nodes.onflow.org:9000"
    },
    "accounts": {
        "emulator-account": {
            "address": "f8d6e0586b0a20c7",
            "key": "2becfbede2fb89796ab68df3ec2a23c3627235ec250a3e5da41df850a8dd4349"
        },
        "testnet-account": {
            "address": "0x8e0dac5df6e8489e",
            "key": "c91f4716a51a66683ccb090ca3eb3e213b90e9f9ae2b1edd12defffe06c57edc"
        }
    },
    "deployments": {}
}

Next, we'll create a new file to write our smart contract.

When writing out the code, you may notice some differences in how Cadence handles NFT creation compared to Solidity. For example, NFTs in Cadence are created as a resource and minted directly into the account of the user. In contrast, Solidity NFTs are essentially just an ID number referenced in a mapping to a specific address on the digital ledger.

So with that in mind, in the same folder as the flow.json file, create a new file called FlowTutorialMint.cdc and type the following code:

/* 
*
*  This is an example implementation of a Flow Non-Fungible Token.
*  This contract does not implement any sophisticated classification
*  system for its NFTs. It defines a simple NFT with minimal metadata.
*   
*/

import NonFungibleToken from 0x631e88ae7f1d7c20
import MetadataViews from 0x631e88ae7f1d7c20

pub contract FlowTutorialMint: NonFungibleToken {

    pub var totalSupply: UInt64

    pub event ContractInitialized()
    pub event Withdraw(id: UInt64, from: Address?)
    pub event Deposit(id: UInt64, to: Address?)

    pub let CollectionStoragePath: StoragePath
    pub let CollectionPublicPath: PublicPath
    pub let MinterStoragePath: StoragePath

    pub struct FlowTutorialMintData{
        pub let id: UInt64
        pub let type: String
        pub let url: String

        init(_id: UInt64, _type: String, _url: String){
            self.id = _id
            self.type = _type
            self.url = _url
        }
    }

    pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
        pub let id: UInt64
        pub let type: String
        pub let url: String

        init(
            id: UInt64,
            type: String,
            url: String,
        ) {
            self.id = id
            self.type = type
            self.url = url
        }

        pub fun getViews(): [Type] {
            return [ Type<FlowTutorialMintData>() ]
        }

        pub fun resolveView(_ view: Type): AnyStruct? {
            switch view {
                case Type<FlowTutorialMintData>():
                return FlowTutorialMintData(
                    _id: self.id,
                    _type: self.type,
                    _url: self.url
                )
            }
            return nil
        }
    }

    pub resource interface FlowTutorialMintCollectionPublic {
        pub fun deposit(token: @NonFungibleToken.NFT)
        pub fun getIDs(): [UInt64]
        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
        pub fun borrowFlowTutorialMint(id: UInt64): &FlowTutorialMint.NFT? {
            post {
                (result == nil) || (result?.id == id):
                    "Cannot borrow FlowTutorialMint reference: the ID of the returned reference is incorrect"
            }
        }
    }

    pub resource Collection: FlowTutorialMintCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
        // dictionary of NFT conforming tokens
        // NFT is a resource type with an `UInt64` ID field
        pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}

        init () {
            self.ownedNFTs <- {}
        }

        // withdraw removes an NFT from the collection and moves it to the caller
        pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")

            emit Withdraw(id: token.id, from: self.owner?.address)

            return <-token
        }

        // deposit takes an NFT and adds it to the collections dictionary
        // and adds the ID to the id array
        pub fun deposit(token: @NonFungibleToken.NFT) {
            let token <- token as! @FlowTutorialMint.NFT

            let id: UInt64 = token.id

            // add the new token to the dictionary which removes the old one
            let oldToken <- self.ownedNFTs[id] <- token

            emit Deposit(id: id, to: self.owner?.address)

            destroy oldToken
        }

        // getIDs returns an array of the IDs that are in the collection
        pub fun getIDs(): [UInt64] {
            return self.ownedNFTs.keys
        }

        // borrowNFT gets a reference to an NFT in the collection
        // so that the caller can read its metadata and call its methods
        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
            return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
        }

        pub fun borrowFlowTutorialMint(id: UInt64): &FlowTutorialMint.NFT? {
            if self.ownedNFTs[id] != nil {
                // Create an authorized reference to allow downcasting
                let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
                return ref as! &FlowTutorialMint.NFT
            }

            return nil
        }

        pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
            let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
            let flowTutorialMintNFT = nft as! &FlowTutorialMint.NFT
            return flowTutorialMintNFT as &AnyResource{MetadataViews.Resolver}
        }

        destroy() {
            destroy self.ownedNFTs
        }
    }

    // public function that anyone can call to create a new empty collection
    pub fun createEmptyCollection(): @NonFungibleToken.Collection {
        return <- create Collection()
    }

     pub fun mintNFT(
            recipient: &{NonFungibleToken.CollectionPublic},
            type: String,
            url: String,
        ) {

            // create a new NFT
            var newNFT <- create NFT(
                id: FlowTutorialMint.totalSupply,
                type: type,
                url: url
            )

            // deposit it in the recipient's account using their reference
            recipient.deposit(token: <-newNFT)

            FlowTutorialMint.totalSupply = FlowTutorialMint.totalSupply + UInt64(1)
        }

    init() {
        // Initialize the total supply
        self.totalSupply = 0

        // Set the named paths
        self.CollectionStoragePath = /storage/flowTutorialMintCollection
        self.CollectionPublicPath = /public/flowTutorialMintCollection
        self.MinterStoragePath = /storage/flowTutorialMintMinter

        // Create a Collection resource and save it to storage
        let collection <- create Collection()
        self.account.save(<-collection, to: self.CollectionStoragePath)

        // create a public capability for the collection
        self.account.link<&FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic, FlowTutorialMint.FlowTutorialMintCollectionPublic, MetadataViews.ResolverCollection}>(
            self.CollectionPublicPath,
            target: self.CollectionStoragePath
        )

        emit ContractInitialized()
    }
}

Important things to note in the smart contract above:

  • We are importing the NonFungibleToken and MetadataViewscontracts to create our NFTs using Flow standards
  • We define our NFT resource in the pub resource NFT function
  • The mintNFT function mints an NFT into the account that calls the function

Now we need to go back into our flow.json file to add a few things:

  • In the contracts section, add the contract and its path.
  • In the deployments section add the network (testnet), the account that we will use to perform the deployment (testnet-account), and the contract name (FlowTutorialMint).
{
    "emulators": {
        "default": {
            "port": 3569,
            "serviceAccount": "emulator-account"
        }
    },
    "contracts": {
        "FlowTutorialMint": "./FlowTutorialMint.cdc"
    },
    "networks": {
        "emulator": "127.0.0.1:3569",
        "mainnet": "access.mainnet.nodes.onflow.org:9000",
        "testnet": "access.devnet.nodes.onflow.org:9000"
    },
    "accounts": {
        "emulator-account": {
            "address": "f8d6e0586b0a20c7",
            "key": "2becfbede2fb89796ab68df3ec2a23c3627235ec250a3e5da41df850a8dd4349"
        },
        "testnet-account": {
            "address": "0x8e0dac5df6e8489e",
            "key": "c91f4716a51a66683ccb090ca3eb3e213b90e9f9ae2b1edd12defffe06c57edc"
        }
    },
    "deployments": {
        "testnet": {
            "testnet-account": [
                "FlowTutorialMint"
            ]
        }
    }
}

The final step in setting up the smart contract is to deploy it to the testnet. To do that, type the following command in the project folder in your terminal:

flow project deploy -n=testnet

We should receive an output stating the contract was deployed successfully:

image6.jpg

It's important to note here that Cadence smart contracts exist in the storage of the account that deploys them, whereas with Solidity, the smart contract exists at its own address on the blockchain.

Although there are limits to the account's storage capacity, these are relative to the amount of FLOW tokens reserved in the account. You can learn more about account storage in the Flow Developer Portal.

Awesome! Now let's build a simple frontend to interact with our contract.

3. Creating The Frontend

For the frontend of this project, we will be using React. First, navigate to a new folder and run the following command to create a React project:

npx create-react-app flow-tutorial

Next, navigate into the flow-tutorial folder and install the Flow Client Library (FCL):

npm i -S @onflow/fcl

The FCL will allow us to communicate with the Flow blockchain, call transactions, and integrate all other FCL-compatible wallets without needing to add custom integrations. Once that finishes, we will install a few additional dependencies:

npm i elliptic sha3 styled-components

After installing all our dependencies, we are ready to start working on the dapp frontend.

3.a. Configure the FCL

Before we begin structuring and styling things, let’s create an FCL configuration file where we’ll define important settings, such as whether we will interact with testnet or mainnet.

In the src directory, create a new folder named flow. Within this new folder, create a file called config.js.

In this config.js file, we will import the FCL, call the fcl.config function and create some settings for our dapp, such as:

  • app.detail.title
  • accessNode.api
  • discovery.wallet

Open the config.js file and fill it with the following code:

const fcl = require("@onflow/fcl");

fcl.config({
  "app.detail.title": "Flow Mint Page Tutorial", // this adds a custom name to our wallet
  "accessNode.api": "https://rest-testnet.onflow.org", // this is for the local emulator
  "discovery.wallet": "https://fcl-discovery.onflow.org/testnet/authn", // this is for the local dev wallet
})

There are additional settings we can configure for our dapp, but for now, this is all we will need.

With the configuration out of the way, let’s move on to building!

3.b. The Initial Structure

First, navigate to the App.js file in the src folder and replace the code with this:

import './App.css';

function App() {

  return (
    <div className="App">
        <h1>Mint Your Dog!</h1>
    </div>
  );
}

export default App;

This will give us the initial structure of our dapp, from which we will expand upon.

Next, we'll style this structure. Open the index.css file and replace the code with the following:

@import url('https://fonts.googleapis.com/css2?family=Michroma&family=Montserrat:wght@200;300;600;700&display=swap');

body {
  margin: 0;
  font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

If you run npm start, you will see a blank page with the title Mint Your Dog!

Next, let's create some components!

3.c. The Nav Component

Inside the src directory, create a new folder called components, where we will build all of our custom React components.

The first component we will create is the Navbar, which will show the Login button if the user isn't connected, or the Logout button next to the user's address and the number of FLOW tokens the account has if they are connected.

Create a file called Navbar.jsx and fill it with the following code:

import * as fcl from "@onflow/fcl";
import styled from "styled-components";
import { useState, useEffect } from "react";
import "../flow/config";

const Wrapper = styled.nav`
  width: -webkit-fill-available;
  background-color: #8dfe89;
  position: fixed;
  top: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 50px;

  button {
    background-color: white;
    padding: 5px 40px;
    max-width: 200px;
    border: none;
    border-radius: 20px;
    font-size: 18px;
    height: 50px;

    &:hover {
      color: white;
      background-color: black;
      cursor: pointer;
    }
  }

  div {
    display: flex;
    gap: 15px;
  }
  box {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
`;

function Navbar() {
  const [user, setUser] = useState({ loggedIn: false, addr: undefined });
  const [flow, setFlow] = useState(0);

  useEffect(() => {
    fcl.currentUser.subscribe(setUser);
    if (user.addr !== "") getFlow(user.addr);
  }, [user.addr]);

  const logOut = async () => {
    await fcl.unauthenticate();
    setUser({ addr: undefined, loggedIn: false });
  };

  const logIn = async () => {
    await fcl.authenticate();
  };

  async function getFlow(address) {
    try {
      const res = await fcl.query({
        cadence: `
                  import FlowToken from 0x7e60df042a9c0868
                  import FungibleToken from 0x9a0766d93b6608b7

                  pub fun main(address: Address): UFix64{
                    let balanceVault =  getAccount(address).getCapability(/public/flowTokenBalance).borrow<&FlowToken.Vault{FungibleToken.Balance}>()!
                    return balanceVault.balance
                  }`,
        args: (arg, t) => [arg(address, t.Address)],
      });
      setFlow(res);
    } catch (error) {
      console.log("err:", error);
    }
  }
  return (
    <Wrapper>
      <h1>Flow Tutorial Mint</h1>
      {user.loggedIn ? (
        <div>
          <button onClick={() => logOut()}>Logout</button>
          <box>
            <span>Address - {user.addr}</span>
            <span>Flow Balance - {flow}</span>
          </box>
        </div>
      ) : (
        <button onClick={() => logIn()}>Login</button>
      )}
    </Wrapper>
  );
}

export default Navbar;

Let's walk through the code to see what's going on here.

  • First, we are importing the Flow Client Library, which will provide us with functions to authenticate, unauthenticate, and determine the currentUser.
  • Next, we import the other dependencies we need and then use styled-components to create the basic styling of our Navbar inside the Wrapper variable.
  • Then, we define some React state variables (user and flow).
  • Next is the functionality of the dapp, such as logOut, logIn, and getFlow (get the connected account's FLOW balance).
  • After that, we return the html for the Navbar wrapped in our styling.

With a complete Navbar component, we can now import it into the App.js file:

import './App.css';
import Navbar from './components/Navbar.jsx';

function App() {

  return (
    <div className="App">
        <Navbar />
        <h1>Mint your Dog!</h1>
    </div>
  );
}

export default App;

Now, if we run the project with npm start, we see our Navbar gives us the functionality we defined in our code. Awesome!

Next, let's build our NFT minting component!

3.d. The NFT Minting Component

Inside the components folder, create a new file called MintComponent.jsx, then copy the following code:

import styled from "styled-components";
import * as fcl from "@onflow/fcl";

const Wrapper = styled.div`
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
  justify-content: center;
  margin-top: 80px;
  padding: 100px;

  main{
    display: flex;
  }

  div{
    width: 300px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
  }

  button{
    width: 100px;
    padding: 10px;
    border: none;
    background-color: #8dfe89;
    border-radius: 20px;
    font-weight: 500;
    &:hover {
      color: white;
      background-color: black;
      cursor: pointer;
    }
  }

  img{
    width: 200px;
  }
`;

function MintComponent() {
  async function mintNFT(type, url) {
    try {
      const res = await fcl.mutate({
        cadence: `
            import FlowTutorialMint from 0x8e0dac5df6e8489e
            import NonFungibleToken from 0x631e88ae7f1d7c20
            import MetadataViews from 0x631e88ae7f1d7c20

            transaction(type: String, url: String){
                let recipientCollection: &FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic}

                prepare(signer: AuthAccount){

                if signer.borrow<&FlowTutorialMint.Collection>(from: FlowTutorialMint.CollectionStoragePath) == nil {
                signer.save(<- FlowTutorialMint.createEmptyCollection(), to: FlowTutorialMint.CollectionStoragePath)
                signer.link<&FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>(FlowTutorialMint.CollectionPublicPath, target: FlowTutorialMint.CollectionStoragePath)
                }

                self.recipientCollection = signer.getCapability(FlowTutorialMint.CollectionPublicPath)
                                            .borrow<&FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic}>()!
                }
                execute{
                    FlowTutorialMint.mintNFT(recipient: self.recipientCollection, type: type, url: url)
                }
            }
            `,
        args: (arg, t) => [arg(type, t.String), arg(url, t.String)],
        limit: 9999,
      });
      fcl.tx(res).subscribe((res) => {
        if (res.status === 4 && res.errorMessage === "") {
            window.alert("NFT Minted!")
            window.location.reload(false);
        }
      });

      console.log("txid", res);
    } catch (error) {
      console.log("err", error);
    }
  }

  return (
  <Wrapper>
    <h1>Mint your Dog!</h1>
    <main>
        <div>
            <img src="https://images.unsplash.com/photo-1517849845537-4d257902454a" alt="Mad Dog"/>
            <h3>Mad Dog</h3>
            <button onClick={() => mintNFT("Mad Dog", "https://images.unsplash.com/photo-1517849845537-4d257902454a")}>Mint</button>
        </div>

        <div>
            <img src="https://images.unsplash.com/photo-1517423568366-8b83523034fd" alt="Swag Dog"/>
            <h3>Swag Dog</h3>
            <button onClick={() => mintNFT("Swag Dog", "https://images.unsplash.com/photo-1517423568366-8b83523034fd")}>Mint</button>
        </div>

        <div>
            <img src="https://images.unsplash.com/photo-1517519014922-8fc06b814a0e" alt="French Dog"/>
            <h3>French Dog</h3>
            <button onClick={() => mintNFT("French Dog", "https://images.unsplash.com/photo-1517519014922-8fc06b814a0e")}>Mint</button>
        </div>
    </main>
  </Wrapper>
  )
}

export default MintComponent;

Again, let’s walk through the code to ensure we understand what’s going on.

  • We need to import the FCL in this component to gain access to the function that will let us mint our NFT.
  • Again, we use styled-components to add some styling.
  • The mintNFT function uses the fcl.mutate function to perform the actual mint by:
    • Validating whether the user has a Flow Tutorial Mint NFT collection in their account and creating one if not.
    • Calling the existing mint function inside the FlowTutorialMint contract and passing the parameters.
    • The function returns the resource (NFT), which we deposit into the user’s account.
  • In the fcl.mutate function, we are importing the smart contract we deployed with the line: import FlowTutorialMint from 0x8e0dac5df6e8489e
  • We also import the NonFngibleToken and MetadataViews standards.
  • In the transaction, we specify the NFT type and url of the image.
  • Cadence transactions have two phases: prepare and execute
    • prepare – we ask for the user’s signature to access their account and perform private functions. In this case, creating a new FlowTutorial Mint collection if they don’t already have one. We also initialize a public Capability restricted to NonFungibleToken.CollectionPublic. For more context on Capabilities, check out this link.
    • execute – call the mintNFT function inside of our contract on the testnet.

In the html portion of the code, we display three images from which the user can mint an NFT.

With our MintComponent complete, we can import it into the App.js file:

import './App.css';
import Navbar from './components/Navbar.jsx';
import MintComponent from './components/MintComponent.jsx';

function App() {

  return (
    <div className="App">
        <Navbar />
        <h1>Mint your Dog!</h1>
        <MintComponent />
    </div>
  );
}

export default App;

Now the user can log into the dapp and mint an NFT to their account!

The final piece of the puzzle is to create a component that will fetch the user’s NFTs and display them.

3.e. Showing The User’s NFTs

In the components folder, create a new file called ShowNfts.jsx, and we will use the following code:

import * as fcl from "@onflow/fcl";
import { useState, useEffect } from "react";
import styled from "styled-components";

const Wrapper = styled.div`
  background-color: #e5e5e5;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
  justify-content: center;
  padding: 50px;

  button {
    width: 100px;
    padding: 10px;
    border: none;
    background-color: #8dfe89;
    border-radius: 10px;
    font-weight: 700;
    &:hover {
      color: white;
      background-color: black;
      cursor: pointer;
    }
  }

  section {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 30px;
    padding: 10%;
  }

  .nftDiv{
    padding: 10px;
    background-color: #141414;
    border-radius: 20px;
    color: white;
    box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.25);
    img{
        width: 140px;
        border-radius: 10px;
    }
    p{
        font-size: 14px;
    }
  }
`;

export default function ShowNfts() {
  const [nfts, setNfts] = useState([]);
  const [user, setUser] = useState({ loggedIn: false, addr: undefined });

    useEffect(() => {
    fcl.currentUser.subscribe(setUser);
    getNFTs(user.addr)
  }, [user.addr]);

  async function getNFTs(addr) {
    try {
      const result = await fcl.query({
        cadence: `
                import FlowTutorialMint from 0x8e0dac5df6e8489e
                import MetadataViews from 0x631e88ae7f1d7c20

                pub fun main(address: Address): [FlowTutorialMint.FlowTutorialMintData] {
                  let collection = getAccount(address).getCapability(FlowTutorialMint.CollectionPublicPath)
                                    .borrow<&{MetadataViews.ResolverCollection}>()
                                    ?? panic("Could not borrow a reference to the nft collection")

                  let ids = collection.getIDs()

                  let answer: [FlowTutorialMint.FlowTutorialMintData] = []

                  for id in ids {

                    let nft = collection.borrowViewResolver(id: id)
                    let view = nft.resolveView(Type<FlowTutorialMint.FlowTutorialMintData>())!

                    let display = view as! FlowTutorialMint.FlowTutorialMintData
                    answer.append(display)
                  }

                  return answer
                }
                `,
        args: (arg, t) => [arg(addr, t.Address)],
      });
      setNfts(result);
    } catch (error) {
      console.log("err", error);
    }
  }

  return (
    <Wrapper>
      <h1>My NFTs</h1>
      <main>
        <button onClick={() => getNFTs(user.addr)}>Get NFTs</button>
        <section>
          {nfts.map((nft, index) => {
            return (
              <div key={index} className="nftDiv">
                <img src={nft.url} alt="nft" />
                <p>Type: {nft.type}</p>
                <p>Id: {nft.id}</p>
              </div>
            );
          })}
        </section>
      </main>
    </Wrapper>
  );
}

Essentially what we are doing in this code is querying the Flow Blockchain using the FCL, and gathering the NFTs in the connected account that are from our FlowTutorialMint collection.

We just need to add this component to our App.js, and we are good to go!

import './App.css';
import Navbar from './components/Navbar.jsx';
import MintComponent from './components/MintComponent.jsx';
import ShowNfts from './components/ShowNfts';

function App() {

  return (
    <div className="App">
      <Navbar />
      <h1>Mint your Dog!</h1>
      <MintComponent />
      <ShowNfts />
    </div>
  );
}

export default App;

That’s everything! Now let’s test our dapp and make sure we can mint some NFTs.

4. Let’s Mint Some NFTs!

So first, let’s start the app with npm start and then open our browser to localhost:3000.

If everything goes well, your screen should look like this:

image8.png

The beautiful thing about using the FCL in our Login sequence is that it gives our users easy access to making an account right on the spot using just an email address. Let's walk through the process to make sure it works properly. By clicking the Login button, a dialogue will pop up, giving us two options to login with. We will choose Blocto.

image7.png

Blocto will prompt us to enter an email address and, upon doing so, gives us the ability to Register a new account. Then, once we input the code emailed to our address, Blocto sets us up with a shiny, new Flow address!

image4.png

From here, we can choose which dog image we want to mint as an NFT. I chose the Swag Dog because it reminds me a bit of myself!

image9.png

Pressing the Mint button will pop up another dialogue telling us about the transaction we are about to perform. We can see that Blocto is graciously covering the minting fees, and if we want to look at the script we are calling, we can do so.

image5.png

Several seconds after hitting Approve, we should receive a message that our mint was successful, and our newly minted Swag Dog will display under the My NFTs section of our dapp.

Here’s a link to our dapp in action:

s1.gifyu.com/images/flow_tutorial-min.gif

The entire source code for this project can be found in this repository.

Conclusion

As you can see, building an NFT minting dapp on the Flow Blockchain is straightforward once you understand how it all works together. Additionally, the Flow Client Library is a powerful tool at our disposal that gives us access to extensive built-in functionality and helps give our dapp a better user experience.

In contrast to Ethereum, Flow handles NFT creation and management much more efficiently and securely. This is achieved by deploying smart contracts and minting the NFTs directly into the user’s account, rather than creating a reference to addresses or mappings stored on the digital ledger.

For more information about building on Flow, check out the Flow Developer Portal.

Have a really great day!