How to Create a Web3 Loyalty Program (with Dynamic NFTs)
In this guide, you'll learn how to build your own NFT loyalty program using thirdweb's Loyalty Card smart contract. This will allow you to:
- Create NFTs
- Issue them to users
- Update their metadata to create a points or rewards system
Let's get started!
Video Tutorial
Prerequisites
Before we begin, make sure you have the following:
- A thirdweb account
- A wallet like MetaMask to connect to thirdweb
- Some test MATIC tokens on the Mumbai testnet to pay gas fees
Step 1: Deploy the Loyalty Card Smart Contract
First, we need to deploy the Loyalty Card smart contract that will power our program.
- Go to the thirdweb dashboard and connect your wallet
- Click 'Deploy New Contract' and search for the 'Loyalty Card' contract
- Give your contract a name, symbol, description and set the Mumbai testnet as the network
- Click 'Deploy Now' and approve the transaction in your wallet
Once deployed, save the contract address somewhere as we'll need it later.
Step 2: Set Up the Project
Next, let's set up our Next.js project:
- Open a terminal and run:
npx thirdweb create app
- Name your project, choose Next.js and TypeScript when prompted
- Change into the project directory:
cd your-project-name
- Open the project in your code editor
- In the
_app.tsx
file, set the active chain to'mumbai'
- Create a new folder called
const
and add a fileaddresses.ts
with the following:
export const LOYALTY_CARD_ADDRESS = 'your-loyalty-card-contract-address';
Step 3: Create a Claim Page
Now let's create a page where users can claim their loyalty card NFT.
- Inside the
pages
folder, create a new fileclaim.tsx
- Add the following code:
import { useState } from 'react';
import { useAddress, useContract, useSDK } from '@thirdweb-dev/react';
import { LOYALTY_CARD_ADDRESS } from '../const/addresses';
export default function Claim() {
const address = useAddress();
const { contract } = useContract(LOYALTY_CARD_ADDRESS);
const sdk = useSDK();
const [isClaiming, setIsClaiming] = useState(false);
async function claim() {
setIsClaiming(true);
try {
const sig = await sdk?.wallet.sign('loyalty-card-claim');
await contract?.erc721.claim(address, sig);
alert('Loyalty Card claimed!');
} catch (err) {
console.error(err);
alert('Failed to claim Loyalty Card');
}
setIsClaiming(false);
}
return (
<div>
<h1>Claim Loyalty Card</h1>
<button disabled={isClaiming} onClick={claim}>
{isClaiming ? 'Claiming...' : 'Claim'}
</button>
</div>
);
}
Here's what this does:
- We use the
useAddress
hook to get the connected wallet address - We use
useContract
to get an instance of the loyalty card contract - We use
useSDK
to get the thirdweb SDK instance - We have a
claim
function that:- Signs a message with the connected wallet
- Calls the
claim
function on the contract, passing the wallet address and signature - Displays an alert if successful or logs an error if it fails
- The component renders a button to claim the loyalty card
Step 4: Create the Profile Page
Next, let's create a profile page that shows the user's loyalty card and points balance.
- Inside the
pages
folder, create a new fileprofile.tsx
- Add the following code:
import { useAddress, useContract, useNFT } from '@thirdweb-dev/react';
import { LOYALTY_CARD_ADDRESS } from '../const/addresses';
export default function Profile() {
const address = useAddress();
const { contract } = useContract(LOYALTY_CARD_ADDRESS);
const { data: nft, isLoading } = useNFT(contract, 0);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Profile</h1>
{nft ? (
<div>
<img src={nft.metadata.image} alt={nft.metadata.name} />
<p>{nft.metadata.name}</p>
<p>Points: {nft.metadata.attributes[0].value}</p>
</div>
) : (
<p>No loyalty card found. Claim one first!</p>
)}
</div>
);
}
Here's what this does:
- We use
useAddress
to get the connected wallet address - We use
useContract
to get the loyalty card contract instance - We use
useNFT
to get the loyalty card NFT data for token ID 0 (assuming the user only has one) - If the NFT exists, we display its image, name, and points balance from the metadata
- If no NFT exists, we show a message prompting the user to claim one
Step 5: Updating Points
The last piece is giving admins a way to update a user's points balance.
- Inside the
pages
folder, create a fileadmin.tsx
- Add this code:
import { FormEvent, useState } from 'react';
import { useContract } from '@thirdweb-dev/react';
import { LOYALTY_CARD_ADDRESS } from '../const/addresses';
export default function Admin() {
const { contract } = useContract(LOYALTY_CARD_ADDRESS);
const [tokenId, setTokenId] = useState('');
const [points, setPoints] = useState('');
async function handleSubmit(e: FormEvent) {
e.preventDefault();
await contract?.call('updatePoints', [parseInt(tokenId), parseInt(points)]);
alert(`Updated token ${tokenId} with ${points} points`);
}
return (
<div>
<h1>Admin - Update Points</h1>
<form onSubmit={handleSubmit}>
<label>
Token ID:
<input
type='number'
value={tokenId}
onChange={(e) => setTokenId(e.target.value)}
/>
</label>
<br />
<label>
Points:
<input
type='number'
value={points}
onChange={(e) => setPoints(e.target.value)}
/>
</label>
<br />
<button type='submit'>Update Points</button>
</form>
</div>
);
}
Here's what this does:
- We use
useContract
to get the loyalty card contract instance - We have a form with fields for token ID and points
- When submitted, it calls the
updatePoints
function on the contract with the provided values - The
updatePoints
function must be implemented in your contract to update the NFT metadata
Step 6: Update the Navbar
To make it easy to navigate between the pages we just created, let's add them to the navbar.
- Open the
components/Navbar.tsx
file - Add the following links:
<Link href='/claim'>
<a>Claim Card</a>
</Link>
<Link href='/profile'>
<a>Profile</a>
</Link>
<Link href='/admin'>
<a>Admin</a>
</Link>
Step 7: Add Styling (Optional)
If you want to style your pages, you can add CSS to the corresponding .module.css
files in the styles
folder. For example, to style the claim page:
- Open
styles/Claim.module.css
- Add your styles, for example:
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.title {
font-size: 2rem;
margin-bottom: 1rem;
}
.button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
background-color: #0070f3;
color: #fff;
font-size: 1rem;
cursor: pointer;
}
.button:hover {
background-color: #0060d9;
}
.button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
- Import and use the styles in
pages/claim.tsx
:
import styles from '../styles/Claim.module.css';
// ...
return (
<div className={styles.container}>
<h1 className={styles.title}>Claim Loyalty Card</h1>
<button
className={styles.button}
disabled={isClaiming}
onClick={claim}
>
{isClaiming ? 'Claiming...' : 'Claim'}
</button>
</div>
);
Repeat this process for the other pages to add your own custom styles.
Conclusion
Congratulations! You've now built a complete NFT loyalty program using the thirdweb Loyalty Card contract and Next.js.
In this guide, we covered how to:
- Deploy the Loyalty Card contract
- Set up a Next.js project with TypeScript
- Create pages for claiming cards, viewing profiles, and updating points
- Add navigation between pages
- Style the pages with CSS modules
That's it! You now have the key components for an NFT-based loyalty program.
What's next?
Some additional features you could add:
- Requiring admin authentication for updating points
- Adding more metadata fields like user info, rank, expiration date, etc.
- Creating a mechanism for users to redeem points for rewards
I hope this guide has been helpful! If you have any questions or feedback, feel free to reach out. Happy building and good luck with your web3 loyalty program?