# Creating a Token-Gated Web Page With Clarity

This tutorial shows how to create a token-gated website using Clarity as the smart contract language. A token-gated website serves content to users with a certain amount of tokens in their wallets.

In this tutorial, we will create two Clarity smart contracts, `FanToken` and `TokenGatedCommunity`. Holders of the `FanToken` can join a community on-chain in the `TokenGatedCommunity` contract, and our frontend will serve users different content based on membership in the `TokenGatedCommunity`.

The diagram below illustrates the functionalities of the smart contracts we will be creating.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711287438371/f97e536e-5074-4bc2-b122-1b58f5def53d.png align="center")

We will get up and running quickly on the front end by creating a NextJS application using a [Stack.js](https://docs.hiro.so/stacksjs-starters) template.

[![Subscribe to the Developer DAO Newsletter. Probably Nothing](https://sitemedia.ams3.digitaloceanspaces.com/blog_banner_v1_d1653cce08.png align="left")](https://devdao.to/blog-newsletter-1)

## Prerequisites

The author assumes the following prerequisites:

* A beginner understanding of Clarity smart contract language
    
* The reader is familiar with the use of a CLI and can navigate between folders via the CLI
    
* Node is installed on the development machine of the reader
    
* Familiarity with React and NextJS
    

### Installing the Clarinet CLI

Clarinet provides a CLI package with a clarity runtime, a REPL, and a testing harness. Clarinet includes a Javascript library, a testing environment, and a browser-based Sandbox. With Clarinet, you can rigorously iterate on your smart contracts locally before moving into production. Install Clarinet [here](https://docs.hiro.so/clarinet/getting-started).

### Installing the Leather wallet

In this tutorial, we will deploy the smart contracts we write to the testnet environment. To do so, we need to create a wallet and acquire some testnet STX. If you haven't already done so, visit [Hiro's wallet page](https://leather.io/) to install the Leather wallet.

> After creating a new wallet, make sure to save your seed phrase somewhere safe. You will need it, along with the password you set when creating the wallet, every time you want to log into your wallet after logging out.

You can request some testnet STX by navigating to [Hiro's testnet explorer sandbox](https://explorer.hiro.so/sandbox/faucet?chain=testnet).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711222495545/b4a7c32e-0813-48ca-87e4-75c914b60072.png align="center")

## Setting up the project

Now that we have created a wallet let's proceed with the project. Create a new directory on your development machine to house the project. This directory will contain the smart contract and frontend application.

```bash
mkdir tokenGateTutorial
```

```bash
cd tokenGateTutorial
```

The Clarity smart contract will be set up inside the project folder `tokenGateTutorial`. The `clarinet` command is used to scaffold a new Clarity project. This command becomes available after the successful installation of Clarity on our development machine.

To scaffold a new Clarity project, run the command below from the location of the `tokenGateTutorial` folder

```bash
clarinet new token-contracts
```

This command scaffolds a new Clarity smart contract project inside a folder called `token-contracts`. The folder name can be called anything you wish. Navigate into the `token-contracts` folder to inspect the files and folders created by the `clarinet new folder-name` command.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711016799804/cfaf7518-eb20-4613-a681-b4cc15ae8181.png align="center")

There is a`contracts` folder where the `FanToken` and `TokenGatedCommunity` contracts will be stored. There is also a `settings` folder that contains three files: `Devnet.toml`, `Mainnet.toml` and `Testnet.toml.` These files contain the seed phrases of wallet accounts used for the deployment of contracts to the different environments of `Devnet`, `Mainnet` and `Testnet respectively`. The project also contains the root `Clarinet.toml` configuration file and our test folder for writing tests.

Let's get to work and write our smart contract in the next section.

## Creating the FanToken contract

Navigate into the `contracts` folder and run the code below to create a new smart contract.

```bash
clarinet contracts new FanToken
```

This command creates our actual Clarity file, `FanToken.clar` inside the contracts directory. This is the location where we will write the `FanToken` contract.

Open the file FanToken.clar, delete the comments inside, and copy and replace the code below into the file.

```lisp
;; traits
(define-trait sip-010-trait
  (
    (transfer (uint principal principal (optional (buff 34))) (response bool uint))
    (get-name () (response (string-ascii 32) uint))
    (get-symbol () (response (string-ascii 32) uint))
    (get-decimals () (response uint uint))
    (get-balance (principal) (response uint uint))
    (get-total-supply () (response uint uint))
    (get-token-uri () (response (optional (string-utf8 256)) uint))
  )
)
```

In this snippet above, we are defining traits for a token. A trait is a standard or an interface to which an implementing token must conform. Top on the trait list is a`transfer` function used to transfer tokens between principals. Addresses in Clarity are referred to as`principal`. There are two types of `principal`:

* `Standard Principal: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM` :
    
    `standard principal` is backed by a corresponding private key used for the signing of transactions. This `principal` starts with the letter ST, denoting that it can only be used in the testnet environment, while the mainnet principal begins with the letter SP and can be used in the mainnet environment.
    
* `Contract Principal: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.FanToken` :
    
    A `contract principal` is the address of the deployed contract. It is a combination of the `standard principal` who deployed the contract and the contract name.
    

Let's continue with the implementation of the token methods. Copy the code below and add it to the file `FanToken.clar`

```lisp

;;previous traits definition above

;; token definitions 
;;one million tokens total supply
(define-fungible-token fanToken u1000000000000) 
;; constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-token-owner (err u101))
(define-constant err-amount-less-than-zero (err u102))

;;#[allow(unchecked_data)]
(define-public (mint (amount uint) (recipient principal))
    (begin
        (asserts! (> amount u0) err-amount-less-than-zero)
        (ft-mint? fanToken amount recipient)
    )
)

;;#[allow(unchecked_data)]
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
    (begin
        (asserts! (> amount u0) err-amount-less-than-zero)
        (asserts! (is-eq tx-sender sender) err-not-token-owner)
        (try! (ft-transfer? fanToken amount sender recipient))
        (match memo to-print (print to-print) 0x)
        (ok true)
    )
)
;; read only functions
(define-read-only (get-balance (owner principal))
    (ok (ft-get-balance fanToken owner))
)
(define-read-only (get-total-supply)
    (ok (ft-get-supply fanToken))
)
(define-read-only (get-token-uri)
    (ok none)
)
(define-read-only (get-name)
    (ok "FanToken")
)
(define-read-only (get-symbol)
    (ok "FT")
)
(define-read-only (get-decimals)
    (ok u6)
)
```

At the top of the code, a fungible token `FanToken` is defined as Clarity has built-in support for fungible and non-fungible tokens (NFTs). The token has a decimal of 6 digits and a total supply of one million. `(define-fungible-token fanToken u1000000000000)`. The `u` in front of the digits represents an unsigned integer type.

After the token definition, we have some declared constant values. The first is the `contract-owner` which is equated to `tx-sender`. `tx-sender` refers to the `principal` executing the transaction. In this instance, the `contract-owner` will be the deployer of the contract. We also define other constants that denote different error messages.

The `mint` function is a public function that allocates tokens to a receiver. The function takes in two parameters of type `unit` and `principal.` Inside the function body, there's a`begin` block used to combine different statements for execution.

The `assert!` block checks for the correctness or truthfulness of a statement. The `asserts!` check is to see if the amount we want to mint exceeds 0. If the `asserts!` block passes, the token amount is minted to the recipient `principal` using the built-in mint function provided by Clarity. `(ft-mint? fanToken amount recipient)`

> This token is set up this way for demonstration purpose as any `principal` can mint the token.

```lisp

(define-public (mint (amount uint) (recipient principal))
    (begin
        (asserts! (> amount u0) err-amount-less-than-zero)
        (ft-mint? fanToken amount recipient)
    )
)
```

The`transfer` function is used to move tokens between different accounts. The `transfer` function takes four arguments, which are the `amount` to transfer, the `sender` (owner) of the token, the `recipient` of the token, and an optional `bytes` variable called `memo`.

Inside the function body, we have a`begin` block that contains other statements written inside it. The first`asserts!` statement checks if the `amount` being transferred is greater than 0, and the next `asserts!` statement checks if `tx-sender` is equal to the `sender` arguments.

If both `asserts!` pass, we attempt to transfer the token using another built-in Clarity method,`ft-transfer`. The transfer of the token is wrapped in a `try` block. The `try` block controls the flow of the program; if the transfer is unsuccessful, the execution halts.

The `match` block checks the provision of the optional parameter, `memo`. If `memo` is passed to the `transfer` function, it binds the value `memo` to the variable`to-print`. The print statement emits the bonded variable. If `memo` was not passed, we write 0x.

At the end of the function, we return a response `(ok true)` indicating that all went well and the changes made in the function can be committed to the blockchain. A public function must return a `response` of either success or failure.

> ```lisp
> 
> (define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
>     (begin
>         (asserts! (> amount u0) err-amount-less-than-zero)
>         (asserts! (is-eq tx-sender sender) err-not-token-owner)
>         (try! (ft-transfer? fanToken amount sender recipient))
>         (match memo to-print (print to-print) 0x)
>         (ok true)
>     )
> )
> ```

We are done discussing the two main functions of the `FanToken` contract, the other functions in the contract are `read-only` that display the token data.

### Testing FanToken in the terminal

We have successfully created a contract, so let's deploy it for testing at the terminal. Navigate to the `contracts` folder located inside the `token-contract` folder. Run the code. This Clarinet CLI command loads and deploys contracts in the terminal environment.

```bash
clarinet console
```

This is a nice way of testing your contract manually before deploying. You can execute the contract's methods from the CLI.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711098799048/bf106b0c-6881-4cf6-b459-376966757897.png align="center")

To test our `mint` function, run the code below at the terminal.

```lisp
(contract-call? .FanToken mint u4000000000 tx-sender)
```

We are calling a contract named; the dot in front of `FanToken` it is a shorthand way of representing the `principal` contract that was deployed. Remember, a contract `principal` is a combination of the `standard principal` that deployed the contract and the contract name. We are executing the contract in the context of the deployer account; that's why the shortcut.`FanToken` works.

The next parameter passed to the `contract-call?` block after the contract name is the function name, which, in our case, is `mint`. Then, the parameters of the function are passed after the function name. We are minting `4000 FT` (FT token uses 6 digits) for the `principaltx-sender.`

Execute the code, and you will get a similar output below. A print block was emitted as the tokens were minted and transferred to `tx-sender`. A `response(ok true)` was returned from the function, showing that the execution was successful.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711100177527/eb3f631a-7e3c-4ca5-890d-ada57b2149e2.png align="center")

We can check the balance of `tx-sender` by executing the `get-balance` function.

```lisp
(contract-call? .FanToken get-balance tx-sender)
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711100581538/16f271bb-bf86-40ad-b575-e44bc16c20f4.png align="center")

## Creating the TokenGatedCommunity contract

We shall proceed to create the second contract`TokenGatedCommunity`. Copy the code below and post it in the `TokenGatedCommunity.char` file located in the contracts folder.

```lisp

;; title: TokenGatedCommunity
;; version: 1.0
;; summary: 
;; description:


;; constants
(define-constant contract-owner tx-sender)
(define-constant err-insufficient-token-balance (err u1002))
(define-constant err-getting-balance (err u1003))
(define-constant err-not-the-owner (err u1004))
(define-constant err-token-not-greaterThan-Zero (err u1005))
(define-constant err-unwrap-failed (err u1006))
(define-constant err-already-a-member (err u1007))
(define-constant err-not-a-member (err u1008))

(define-map tokenBalances principal uint)

;; data vars
(define-data-var entryTokenAmount uint u50000000) ;;50 FT


(define-public (joinCommunity) 
  (begin
    (if (is-none (userTokenBalance tx-sender))
       (transferTokenAndJoinCommunity (var-get entryTokenAmount))
      err-already-a-member
    )
  )
)

 (define-private (transferTokenAndJoinCommunity (tokenAmount uint)) 
   (begin 
    (try! (contract-call? .FanToken transfer tokenAmount tx-sender (as-contract tx-sender) none))
    (map-set tokenBalances tx-sender tokenAmount)
    (ok true)
   )
 )

 (define-private (payoutTokens (receiver principal) (amount uint))
     (begin 
        (asserts! (is-eq .TokenGatedCommunity (as-contract tx-sender)) err-not-the-owner )
        (try! (as-contract (contract-call? .FanToken transfer amount .TokenGatedCommunity receiver  none)))
        (ok true)
      )
  )

 (define-public (removeTokenAndExitCommunity)
     (
        let (
            (balance (unwrap! (userTokenBalance tx-sender) err-not-a-member))
        )

        (try! (payoutTokens tx-sender balance))
        (map-delete tokenBalances tx-sender)
        (ok true)
     )
 )

;;public-function
(define-public (setEntryTokenAmount (newEntryTokenAmount uint) ) 
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-not-the-owner)
        (asserts! (> newEntryTokenAmount u0) err-token-not-greaterThan-Zero)
        (var-set entryTokenAmount newEntryTokenAmount)
        (ok true)
     )
)

(define-read-only (userTokenBalance (user principal)) 
   (map-get? tokenBalances user)
)

(define-read-only (isUserACommunityMember (user principal)) 
   (match (map-get? tokenBalances user) bal
     (begin 
       bal
      (ok true)
     )
     (ok false)
   )
)

;;read-only function
(define-read-only (getEntryTokenAmount)
     (ok (var-get entryTokenAmount))
)
```

At the top of the contract, some useful constants are defined. The `contract-owner` is set to the contract deployer. Some other declared constant variables are used to represent errors.

A data variable `(define-data-var entryTokenAmount uint u50000000)` called `entryToken` is defined, and its initial value is set at 50 FT tokens.

The functionality of this contract;

* users send `FanToken` to the contract, and they are registered in the community; their principal is added to a `data-map` variable `tokenBalances`
    
* users can withdraw their `FanToken` from the `TokenGateCommunity` contract, and their principal is deleted from the `data-map`.

### Sending tokens to the `TokenGateCommunity` Contract

The public function `joinCommunity` adds the user to the community if they are not already a member. Let's dissect the function to see how it works

```lisp
(define-public (joinCommunity) 
  (begin
    (if (is-none (userTokenBalance tx-sender))
      (transferTokenAndJoinCommunity (var-get entryTokenAmount))
      err-already-a-member
    )
  )
)
```

The function accepts no parameter; inside the `begin` block, a `read-only` function `userTokenBalance` returns a value of an `optional` from the variable`tokenBalances` of the user ( caller of the function ). The return value of `userTokenBalace` could either be a `(some unit)` if the user `principal` is on the `data-map` variable or`none` if it is not on the data-map variable.

```lisp
(define-read-only (userTokenBalance (user principal)) 
   (map-get? tokenBalances user)
)
```

The `if block` checks the boolean value of the result from the `is-none` block. If the result from `userTokenBalance` is `noneis-none` block returns `true` and if the result is `(some unit)is-none` block returns `false`.

The if block runs when the user is not already a community member. The private function `transferTokenAndJoinCommunity` is executed. The function is accepted as a parameter; this is the entry fee used to join the community.

An external `contract-call?` is made to the `.FanToken` contract, calling its `transfer` function. An amount of tokens equal to the parameter `tokenAmount` is transferred from the user account to the `TokenGatedCommunity` contract. This transfer is wrapped in a `try!` block that halts contract execution if the token transfer fails.

Next, the user is added to the `data-map` , a `response` of `ok` is returned to the function caller. The token has been transferred successfully to the `TokenGatedCommunity` contract, and the user is now a community member.

The remaining portion left to dissect in the `joinCommunity` function is the `else` part of the `if` block. This throws an error stating that the user is already a community member.

```lisp

(define-private (transferTokenAndJoinCommunity (tokenAmount uint)) 
   (begin 
    (try! (contract-call? .FanToken transfer tokenAmount tx-sender (as-contract tx-sender) none))
    (map-set tokenBalances tx-sender tokenAmount)
    (ok true)
   )
 )
```

### Withdrawing tokens from the `TokenGateCommunity` Contract

This is when the user gets tired of the community and wants to leave. Ideally, they should get their tokens back from the `TokenGateCommunity` contract and their `principal` deleted from the `userTokenBalancesdata-map`.

Let's see the implementation details:

```lisp
 (define-public (removeTokenAndExitCommunity)
     (
        let (
            (balance (unwrap! (userTokenBalance tx-sender) err-not-a-member))
        )
        (try! (payoutTokens tx-sender balance))
        (map-delete tokenBalances tx-sender)
        (ok true)
     )
 )
```

The public function `removeTokenAndExitCommunity` is called by the user to exit the community and get paid their tokens. Inside the function, there's a `let` block used to save values in a temporary data-binding variable called `balance`. The `unwraps!` function unwraps the `optional` value returned from the read-only function `userTokenBalance.` If unwrap was successful, the user token balance in the contract is stored in the temp variable `balance` but if `unwrap!` encounters a value of `none` in the function, meaning the `principal` is not in the `tokenBalancesdata-map`, it throws an `err-not-a-member` error.

After retrieving the user token stored by the contract, the private function `payoutTokens` is called with two parameters of the user `principal` and the user token amount in the contract. Let's examine the `payoutTokens` function below:

```lisp
  (define-private (payoutTokens (receiver principal) (amount uint))
     (begin 
        (asserts! (is-eq .TokenGatedCommunity (as-contract tx-sender)) err-not-the-owner )
        (try! (as-contract (contract-call? .FanToken transfer amount .TokenGatedCommunity receiver  none)))
        (ok true)
      )
    )
```

The function starts with a `begin` block, which contains three statements. The first is an `asserts!` function that checks the equality of the `contract principal` with the caller; the `(as-contract tx-sender)` calls a function in the context of the contract and not the wallet `principal`.

The next statement makes an external call to the `transfer` function of `.FanToken` contract in the context of the `.TokenGatedCommunity` contract. This is necessary to transfer the tokens, as the tokens are owned by the `.TokenGatedCommunity` contract at this point. We return a `response` of `ok` if the tokens were successfully transferred to the recipient.

Finally in the `removeTokenAndExitCommunity` function, the user is deleted from the `tokenBalancesdata-map` and a `response` of `ok` is returned from the function.

### Setting the `entryTokenAmount` value

The function sets the value of the data variable `entryTokenAmount.` The `setEntryToken` function accepts a `newEntryTokenAmount` as a parameter with a type of `unit`. The first `asserts!` checks that the `contract-ownerconstant` is the same as the `principal` calling the function. The next `asserts!` performs data sanitation, ensuring that the passed parameter is greater than `0.` The value of `entryTokenAmount` is changed using the `var-set` function. A `response` of type `(ok true)` is returned from the function.

```lisp
;;public-function
(define-public (setEntryTokenAmount (newEntryTokenAmount uint) ) 
(begin
    (asserts! (is-eq tx-sender contract-owner) err-not-the-owner)
    (asserts! (> newEntryTokenAmount u0) err-token-not-greaterThan-Zero)
    (var-set entryTokenAmount newEntryTokenAmount)
    (ok true)
)
)
```

Wooh! We have come to the end of creating the smart contracts. You can test this contract the same way we tested the `FanToken` contract.

## Deploying Clarity smart contracts to testnet

Navigate to the `settings` folder in the Clarity project. The folder contains three files `Devnet.toml`, `Testnet.toml` and `Mainnet.toml` contained inside the contracts folder. These files will contain the seed phrases for the wallet that will deploy the contract in the respective environment of `Devnet`, `Testnet` and `Mainnet`. `Devnet.toml` is already filled with different wallet seed phrases.

Open the `Testnet.toml` and replace "`<YOUR PRIVATE TESTNET MNEMONIC HERE>`" with the seed phrase from the wallet we created earlier.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711223560574/07967f5b-4886-44ba-95a2-9ccbbfc6368b.png align="center")

Move into the `contracts` folder of the `Clarinet` project and run the command below to generate a deployment file for the testnet environment.

```bash
clarinet deployment generate --testnet --low-cost
```

This command generates a deployment file for `testnet` using low-cost fees for the deployment. If we are deploying to `mainnet`, we change `--testnet` to `--mainnet`.

After creating the deployment file, the next step is to apply the file and deploy the contract to the chosen environment. This command applies the deployment:

```bash
clarinet deployment apply -p deployments/default.testnet-plan.yaml
```

The smart contracts are broadcast and deployed to the network. Our contract name is a combination of the `principal` that deployed the contract and the contract name.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711224388782/ed71c388-f4aa-43f2-8859-9b5734f2f130.png align="center")

### Deploying to devnet

We will also deploy the smart contracts to devnet. Hiro has a wonderful explorer that can be used to simulate and test the functionality of contracts on the devnet. Deploying to the devnet is straightforward. Before proceeding, you should install Docker on your system. If you haven't installed it yet, you can i[nstall it from the official page](https://docs.docker.com/get-docker/).

If Docker has been installed, open the contracts folder of the Clarity projects at the terminal and run:

```bash
clarinet devnet start
```

Docker will download the containers needed to run the devnet the first time you run this command. It might take a little time, so be patient. Your terminal will look similar to the one below when the devnet is running. Stacks Devnet Explorer runs on `localhost:8000`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711395131165/0f6519a7-779f-44e7-804e-bcbd96e77935.png align="center")

Keep the devnet terminal running, and let's proceed to the frontend to connect our application to the devnet blockchain.

## Creating a NextJS frontend

The smart contracts part of our application is ready. The next step is to create a NextJS frontend that will interact with the contracts. We will use [Hiro starter templates](https://docs.hiro.so/stacksjs-starters) for fast iteration.

Navigate to the root folder of your project and create a new folder named `frontend`. `cd` into the `frontend` folder and run the code below to create the NextJS application.

```bash
npm create stacks --template .
```

The `stacks` tool will walk you through creating a NextJS project. The period `(.)` after the word `--template` means we want the NextJS application to be installed in the current directory. Enter `frontend` as the package name when prompted by the CLI and follow the other prompts to install a NextJS application.

Next, install dependencies by running the command below to install the application dependencies.

```bash
npm install
```

The frontend application is started by running `npm run dev`. The starter template makes our integration faster as it contains useful boilerplate code, which we can modify to suit our needs.

Let's do a little bit of housekeeping. You can delete the `ContractVote.js` file found in `src/components` folder. Delete the content of `ConnectWallet.js` file located in the components directory and replace it with the below code;

```javascript
"use client";

import React, { useEffect, useState } from "react";
import { AppConfig, showConnect, UserSession } from "@stacks/connect";
import styles from "../app/page.module.css";

const appConfig = new AppConfig(["store_write", "publish_data"]);

export const userSession = new UserSession({ appConfig });

const trimAddress = (address) => {
  if (address) {
    const start = address.substr(0, 6);
    const middle = ".....";
    const end = address.substr(address.length - 6, address.length)
    return `${start}${middle}${end}`
  }
  return null;
}

function authenticate() {
  showConnect({
    appDetails: {
      name: "Token Gated Demo",
      icon: window.location.origin + "/logo512.png",
    },
    redirectTo: "/",
    onFinish: () => {
      window.location.reload();
    },
    userSession,
  });
}

function disconnect() {
  userSession.signUserOut("/");
}

const ConnectWallet = () => {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);

  if (mounted && userSession.isUserSignedIn()) {
    return (
      <div className="Container">
        <button className={styles.buttonConnected} onClick={disconnect}>
          Disconnect Wallet {trimAddress(userSession.loadUserData().profile.stxAddress.testnet)}
        </button>
      </div>
    );
  }

  return (
    <button className={styles.button} onClick={authenticate}>
      Connect Wallet
    </button>
  );
};

export default ConnectWallet;
```

The addition we made to the file is the `trimAddress` function used to trim the connected wallet `principal`. We won't be discussing any `CSS` styles in this integration.

The `ConnectWallet` component shows a button asking a user to connect their wallet. Most of the code is boilerplate, and we won't spend time discussing it. The meat of the `frontend` application will be in the `page.js` located in the app folder.

Open the `page.js` file and replace its content following code:

```javascript
"use client";
import { useEffect, useState } from "react";
import styles from "./page.module.css";
import { Connect } from "@stacks/connect-react";
import ConnectWallet, { userSession } from "/../components/ConnectWallet";

const deployer = "replace-with-the-account-that-deployed-the-project" //acount that deployed the contract
export default function Home() {
  const [isClient, setIsClient] = useState(false);
  const [isQualified, setIsQualified] = useState(false);
  const [userTokenAmount, setUserTokenAmount] = useState(0)
  const [tokensToMint, setTokensToMint] = useState(0);
  const [tokenReceiver, setTokenReceiver] = useState(null)


  useEffect(() => {
    setIsClient(true);
  }, []);


  if (!isClient) return null;

  return (
    <Connect
      authOptions={{
        appDetails: {
          name: "Token Gated Community",
          icon: window.location.origin + "/logo.png",
        },
        redirectTo: "/",
        onFinish: () => {
          window.location.reload();
        },
        userSession,
      }}
    >
      <div className={styles.container}>
        <header className={styles.header}>
          <h1>Token Gated Website</h1>
          <nav className={styles.nav}>

            <ConnectWallet />

          </nav>
        </header>
        <main>

  
          <div className={styles.inputContainerColumn}>
            <p>Mint FT Tokens</p> &nbsp;
            <input className={styles.input} value={tokensToMint}
              onChange={(e) => setTokensToMint(e.target.value)}
              type="number" placeholder="00"
            />
            <input className={styles.input}
              type="text"
              placeholder="principal to receive the token"
              value={tokenReceiver}
              onChange={(e) => setTokenReceiver(e.target.value)}

            />
            <button className={styles.button}>Mint FT Tokens</button>
          </div>


          {!isQualified &&
            <div className={styles.container}>
              <h3>Join Community</h3>

              <button className={styles.button}
                >Join Community</button>
            </div>
          }



          {/*Token gated content */}

          {isQualified &&

            <div>
              <h1>Welcome to the community of dog lovers Premium content</h1>
              <h3>This part of the website is token gated!</h3>
              <div className={styles.card}>
                <p>Send a transaction to change the entryTokenAmount variable</p>
                <div className={styles.inputContainer}>
                  <input type="number" className={styles.input} placeholder="00" />
                  <button className={styles.button}>Change Entry Fee</button>
                </div>
              </div>



              <div className={styles.inputContainer}>
                <p>Exit Community and claim back your tokens</p>

                <button className={styles.button}>Exit Community</button>
              </div>

            </div>

          }


        </main>
      </div>
    </Connect>
  );
}
```

The application should look like the one below. At the top of the file, we specified that the file will only run in the client environment, `useState` and `useEffect` is imported from react.

Some `react` state is defined that will be used in the application. Look at the `deployer` variable; replace this value with the account that deployed the contract. If working with devnet this will be the first account listed in the `Devnet.toml` file.

The `ConnectWallet` button was imported, as well as a `Connect` component from the package `@stacks/connect-react`. The `Connect` component manages the user session as they sign out and sign in via their wallets.

The simple UI of the front end will allow a user to mint `FanToken`, join the community, and exit the community. The `isQualified` state renders the token-gated portion of the website if its value is `true`. Let's see how to read contract value from a Clarity smart contract in the frontend application.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711397951218/140e8b8b-3298-4ef7-b7a4-39f9d5c1b858.png align="center")

### Reading values from Clarity smart contracts using Stacks.js

The first function we shall implement reads the amount of FanToken owned by the signed-in user. Copy and paste the code below in the `page.js` file after the useEffect function.

```javascript
 import { StacksTestnet, StacksDevnet } from "@stacks/network";
 import { callReadOnlyFunction, standardPrincipalCV } from '@stacks/transactions';

//code removed here

  useEffect(() => {
    setIsClient(true);
  }, []);

const getTokenBalance = async () => {
    const userDevnetAddress = userSession.loadUserData().profile.stxAddress.testnet
    console.log("devNetAddress ", userDevnetAddress)
    try {
      const contractName = 'FanToken';
      const functionName = 'get-balance';
      const network = new StacksDevnet();
      const senderAddress = userDevnetAddress;
      const options = {
        contractAddress: deployer,
        contractName,
        functionName,
        functionArgs: [standardPrincipalCV(userDevnetAddress)],
        network,
        senderAddress,
      };
      const result = await callReadOnlyFunction(options);
      const { value: { value } } = result
      const tokenAmount = +value.toString() / 1_000_000
      setUserTokenAmount(tokenAmount)
      console.log("result => ", tokenAmount + "FT")
    } catch (error) {
      console.log("error => ", error);
    }
  };

//code remove for brevity
```

The `getTokenBalance` gets the balance of the user token. This is a read-only function, so we ain't making a transaction. We imported `StacksDevnet` network from the `@stacks/network`. If the contract we are working with is deployed on a `testnet`, we will use the `StacksTestnet` network constructor.

The connected user wallet is retrieved from the `userSession` object, and an options object that contains the following properties is constructed;

```javascript
 const options = {
        contractAddress: deployer,
        contractName,
        functionName,
        functionArgs: [standardPrincipalCV(userDevnetAddress)],
        network,
        senderAddress,
      };
```

Function arguments have to be converted to `Clarity` type. The `standardPrincipalCV` converts the user address to a `Clarity principal` type. Next, we call the contract, passing the `options` object we have defined and `await` the result of the call.

```javascript
const result = await callReadOnlyFunction(options);
```

The result is an object that contains the user balance sent from the smart contract we just read. We destructure the result object and divide the value obtained by `100000` to get the token amount, as the `FanToken` operates internally with 6 digits. Lastly, we update the state using `setUserTokenAmount` function.

```javascript
const { value: { value } } = result
const tokenAmount = +value.toString() / 1_000_000
setUserTokenAmount(tokenAmount)
```

The`getTokenBalance` function is triggered in a `useEffect` when the user sign in in with the wallet. Add this useEffect code;

```javascript
  useEffect(() => {
    getTokenBalance()
    isUserACommunityMember()
  }, [userSession.isUserSignedIn()])
```

Let's define another read-only function that checks if the user is a community member, `isUserACommunityMember`. Copy and paste the code below in the page.js file.

```javascript
import {
  callReadOnlyFunction, standardPrincipalCV, ClarityType, AnchorMode,
  PostConditionMode, contractPrincipalCV, uintCV
} from '@stacks/transactions';
//top of code removed. paste after the getTokenBalance function
 const isUserACommunityMember = async () => {
    const userDevnetAddress = userSession.loadUserData().profile.stxAddress.testnet
    try {
      const contractName = 'TokenGatedCommunity';
      const functionName = 'isUserACommunityMember';
      const network = new StacksDevnet();
      const senderAddress = userDevnetAddress;
      const options = {
        contractAddress: deployer,
        contractName,
        functionName,
        functionArgs: [standardPrincipalCV(userDevnetAddress)],
        network,
        senderAddress,
      };
      const result = await callReadOnlyFunction(options);
      if (result.value.type == ClarityType.BoolTrue) {
        setIsQualified(true)
      } else {
        setIsQualified(false)
      }
    } catch (error) {
      console.log("error => ", error);
    }
  };
```

This function calls the `isUserACommunityMember` function of the `TokenGatedCommunity` contract. The function returns a boolean indicating whether the user is a member. The state `isQualified` is set based on the passed boolean result.

### Sending transactions to a Clarity contract using Stacks.js

We will examine how to send transactions to our smart contracts. Previously, we saw how to obtain values from the contract via read-only functions. This section will examine the `mintToken` function to see how this is done. The mintFunction is used to mint tokens; it accepts a token amount and the receiver address as parameters. The user needs to have some tokens before joining the community, so the `mintToken` function gives the user free tokens.

```javascript
const mintTokens = async () => {
    let amount = tokensToMint * 1000_000; //token is 6 digit
    amount = uintCV(amount);
    openContractCall({
      network: new StacksDevnet(), //on testnet new StacksTestnet()
      anchorMode: AnchorMode.Any,
      contractAddress: deployer,
      contractName: 'FanToken',
      functionName: 'mint',
      functionArgs: [amount, standardPrincipalCV(tokenReceiver)],
      postConditionMode: PostConditionMode.Deny,
      postConditions: [],
      onFinish: data => {
        // WHEN user confirms pop-up
        setTokensToMint(0)
        setTokenReceiver(null)
        //on testnet the url will be => https://explorer.hiro.so/txid/${data.txId}?chain=testnet
        window
          .open(
            `http://localhost:8000/txid/${data.txId}?chain=testnet&api=http://localhost:3999`,
            "_blank"
          )
          .focus();
      },
      onCancel: () => {
        // WHEN user cancels/closes pop-up
        console.log("onCancel:", "Transaction was canceled");
      },
    });
  }
```

Transactions are sent using the `openContractCall` Stacks.js function. The function accepts as an argument an object that contains the `functionName`, `contractAddress` like the `callReadOnlyFunction` we used earlier to get the token balance. It also contains additional properties like `postConditionMode`, `postCondition` , `onFinish` and `onCancel`.

The `postCondistionMode` has a value of `PostConditionMode.Deny` and `PostConditionMode.Allow`. `PostConditionMode.Deny`means that tokens will not be transferred from the user. This protects the user against malicious contracts, as the transaction will revert if any such token transfer is attempted. The `postCodition` array sets conditions specifying how many tokens can be withdrawn from the user.

`onFinish` function is triggered after the transaction has been created; in this example, we open the explorer to view the transaction. `onCancel` is triggered if the user cancels the transaction. The value of the amount of token we are minting is retrieved from the `tokenToMint` state, and it is converted to a `Clarity` type of `uintCV,` so also the token receiver value is converted to a `standardPrincipal` and both values are passed to the `functionArgs` array.

After minting the tokens, we can refresh the page to see our minted token. There are two more transactions to define for this webpage: a transaction function to join the community and another function to exit the community.

### Finishing the frontend

The complete code of the `page.js` function is given below. We define a function `joinTokenGatedCommunity` and `exitTokenGatedCommunity` and some other utility functions. The `isQualified` state regulates access to the token-gated parts of the webpage. `isQualified` is set based on the value returned from the `read-only` function `isUserACommunityMember`.

Adding the complete code to `page.js`, we will see that there is some restricted parts of our webpage. We can build on this idea to create a more robust web application for token-gated users.

```javascript
"use client";
import { useEffect, useState } from "react";
import styles from "./page.module.css";

import { Connect } from "@stacks/connect-react";

import ConnectWallet, { userSession } from "../components/ConnectWallet";
import { StacksTestnet, StacksDevnet } from "@stacks/network";
import {
  callReadOnlyFunction, standardPrincipalCV, ClarityType, AnchorMode,
  PostConditionMode, contractPrincipalCV, uintCV
} from '@stacks/transactions';
import { openContractCall } from '@stacks/connect';


const deployer = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM" //acount that deployed the contract

export default function Home() {
  const [isClient, setIsClient] = useState(false);
  const [isQualified, setIsQualified] = useState(false);
  const [userTokenAmount, setUserTokenAmount] = useState(0)
  const [tokensToMint, setTokensToMint] = useState(0);
  const [tokenReceiver, setTokenReceiver] = useState(null)

  const mintTokens = async () => {
    let amount = tokensToMint * 1000_000; //token is 6 digit
    amount = uintCV(amount);
    openContractCall({
      network: new StacksDevnet(), //on testnet new StacksTestnet()
      anchorMode: AnchorMode.Any,
      contractAddress: deployer,
      contractName: 'FanToken',
      functionName: 'mint',
      functionArgs: [amount, standardPrincipalCV(tokenReceiver)],

      postConditionMode: PostConditionMode.Deny,
      postConditions: [],

      onFinish: data => {
        // WHEN user confirms pop-up
        setTokensToMint(0)
        setTokenReceiver(null)
        //on testnet the url will be => https://explorer.hiro.so/txid/${data.txId}?chain=testnet
        window
          .open(
            `http://localhost:8000/txid/${data.txId}?chain=testnet&api=http://localhost:3999`,
            "_blank"
          )
          .focus();
      },
      onCancel: () => {
        // WHEN user cancels/closes pop-up
        console.log("onCancel:", "Transaction was canceled");
      },
    });

  }


  const joinTokenGatedCommunity = async () => {
    try {
      openContractCall({
        network: new StacksDevnet(), //on testnet new StacksTestnet()
        anchorMode: AnchorMode.Any,
        contractAddress: deployer,
        contractName: 'TokenGatedCommunity',
        functionName: 'joinCommunity',
        functionArgs: [],

        postConditionMode: PostConditionMode.Allow,
        postConditions: [],
        onFinish: data => {
          setTokenReceiver(null);
          setTokensToMint(0)
          // WHEN user confirms pop-up
          //on testnet the url will be => https://explorer.hiro.so/txid/${data.txId}?chain=testnet
          window
            .open(
              `http://localhost:8000/txid/${data.txId}?chain=testnet&api=http://localhost:3999`,
              "_blank"
            )
            .focus();
        },
        onCancel: () => {
          // WHEN user cancels/closes pop-up
          console.log("onCancel:", "Transaction was canceled");
        },
      });
    } catch (error) {
      console.log("error => ", error);
    }
  };

  const exitTokenGatedCommunity = async () => {
    try {
      openContractCall({
        network: new StacksDevnet(), //on testnet new StacksTestnet()
        anchorMode: AnchorMode.Any,
        contractAddress: deployer,
        contractName: 'TokenGatedCommunity',
        functionName: 'removeTokenAndExitCommunity',
        functionArgs: [],
        postConditionMode: PostConditionMode.Allow,
        postConditions: [],
        onFinish: data => {
          // WHEN user confirms pop-up
          //on testnet the url will be => https://explorer.hiro.so/txid/${data.txId}?chain=testnet
          window
            .open(
              `http://localhost:8000/txid/${data.txId}?chain=testnet&api=http://localhost:3999`,
              "_blank"
            )
            .focus();
        },
        onCancel: () => {
          // WHEN user cancels/closes pop-up
          console.log("onCancel:", "Transaction was canceled");
        },
      });
    } catch (error) {
      console.log("error => ", error);
    }
  };

  const getTokenBalance = async () => {
    const userDevnetAddress = userSession.loadUserData().profile.stxAddress.testnet
    console.log("devNetAddress ", userDevnetAddress)
    try {
      const contractName = 'FanToken';
      const functionName = 'get-balance';
      const network = new StacksDevnet();
      const senderAddress = userDevnetAddress;
      const options = {
        contractAddress: deployer,
        contractName,
        functionName,
        functionArgs: [standardPrincipalCV(userDevnetAddress)],
        network,
        senderAddress,
      };
      const result = await callReadOnlyFunction(options);
      const { value: { value } } = result
      const tokenAmount = +value.toString() / 1_000_000
      setUserTokenAmount(tokenAmount)
      console.log("result => ", tokenAmount + "FT")
    } catch (error) {
      console.log("error => ", error);
    }
  };

  const isUserACommunityMember = async () => {
    const userDevnetAddress = userSession.loadUserData().profile.stxAddress.testnet
    try {
      const contractName = 'TokenGatedCommunity';
      const functionName = 'isUserACommunityMember';
      const network = new StacksDevnet();
      const senderAddress = userDevnetAddress;
      const options = {
        contractAddress: deployer,
        contractName,
        functionName,
        functionArgs: [standardPrincipalCV(userDevnetAddress)],
        network,
        senderAddress,
      };
      const result = await callReadOnlyFunction(options);
      if (result.value.type == ClarityType.BoolTrue) {
        setIsQualified(true)
      } else {
        setIsQualified(false)
      }

    } catch (error) {
      console.log("error => ", error);
    }
  };


  useEffect(() => {
    setIsClient(true);
  }, []);

  useEffect(() => {
    getTokenBalance()
    isUserACommunityMember()
  }, [userSession.isUserSignedIn()])

  if (!isClient) return null;

  return (
    <Connect
      authOptions={{
        appDetails: {
          name: "Token Gated Community",
          icon: window.location.origin + "/logo.png",
        },
        redirectTo: "/",
        onFinish: () => {
          window.location.reload();
        },
        userSession,
      }}
    >
      <div className={styles.container}>
        <header className={styles.header}>
          <h1>Token Gated Website</h1>
          <nav className={styles.nav}>

            <ConnectWallet />

          </nav>
        </header>
        <main>

          <p>
            User Fan Token Balance: {userTokenAmount} FT
          </p>


          <div className={styles.inputContainerColumn}>
            <p>Mint FT Tokens</p> &nbsp;
            <input className={styles.input} value={tokensToMint}
              onChange={(e) => setTokensToMint(e.target.value)}
              type="number" placeholder="00"
            />
            <input className={styles.input}
              type="text"
              placeholder="principal to receive the token"
              value={tokenReceiver}
              onChange={(e) => setTokenReceiver(e.target.value)}

            />
            <button className={styles.button} onClick={mintTokens}>Mint FT Tokens</button>
          </div>


          {!isQualified &&
            <div className={styles.container}>
              <h3>Join Community</h3>

              <button className={styles.button}
                onClick={joinTokenGatedCommunity}>Join Community</button>
            </div>
          }

          {/*Token gated content */}

          {isQualified &&

            <div>
              <h1>Welcome to the community of dog lovers Premium content</h1>
              <h3>This part of the website is token gated!</h3>
              <div className={styles.card}>
                <p>Send a transaction to change the entryTokenAmount variable</p>
                <div className={styles.inputContainer}>
                  <input type="number" className={styles.input} placeholder="00" />
                  <button className={styles.button}>Change Entry Fee</button>
                </div>
              </div>



              <div className={styles.inputContainer}>
                <p>Exit Community and claim back your tokens</p>

                <button className={styles.button}
                  onClick={exitTokenGatedCommunity}>Exit Community</button>
              </div>

            </div>

          }
        </main>
      </div>
    </Connect>
  );
}
```

[![Subscribe to the Developer DAO Newsletter. Probably Nothing](https://sitemedia.ams3.digitaloceanspaces.com/blog_banner_v1_d1653cce08.png align="left")](https://devdao.to/blog-newsletter-1)

## Conclusion

This example application could be built upon to solidify the knowledge gained in integrating `Clarity` smart contracts with a frontend application.

In this blog post, we learned how to create `Clarity` smart contracts, fungible tokens, and how to token-gate a webpage using a token. We also learned how to deploy smart contracts to either the `testnet` or `devnet` and connect with a front-end to send transactions to the network. I hope you have learned a thing or two from this piece.

Thank you for reading to the end. The source code can be found [here](https://github.com/jamiebones/clarity-token-gated-example).
