Skip to content

Commit

Permalink
Merge pull request #22 from open-format/feature/token-based-access
Browse files Browse the repository at this point in the history
Feature: Token based access
  • Loading branch information
tinypell3ts authored Jan 5, 2024
2 parents f0ebafd + a4c3f0c commit b897f2a
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 23 deletions.
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ generate-env-files:
@cp backend/.env.example backend/.env
@cp frontend/.env.local.example frontend/.env.local
@echo "Environment files copied..."
@echo "Update backend/.env and frontend/.env.local"

setup: install generate-env-files db
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ cd get-started
make setup
```

4. Add environment variables to `./backend/.env`
4. Add environment variables to `./backend/.env` and `./frontend/.env.local`

5. Execute development commands:

Expand Down
4 changes: 2 additions & 2 deletions backend/src/constants/missions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export default [
{
id: "test_mission",
id: "Reward User",
description:
"Complete two test actions to complete the Test Mission",
"Press Reward User on the App page twice to complete this Quest.",
tokens: [
{
address: process.env.XP_TOKEN_ID,
Expand Down
4 changes: 2 additions & 2 deletions backend/src/routes/api/v1/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ auth.post("/verify", async (c) => {
},
});

console.log({ result });

const originalChallenge = result ? result.challenge : null;

if (!originalChallenge) {
Expand Down Expand Up @@ -88,6 +86,7 @@ auth.post("/verify", async (c) => {
status: Status.SUCCESS,
access_token: accessToken,
refresh_token: refreshToken,
address: eth_address,
});
} else {
return c.json(
Expand Down Expand Up @@ -143,6 +142,7 @@ auth.post("/refresh-token", async (c) => {
return c.json({
status: Status.SUCCESS,
refresh_token,
address: tokenRecord.user.eth_address,
access_token: newAccessToken,
});
});
Expand Down
Binary file modified bun.lockb
Binary file not shown.
13 changes: 13 additions & 0 deletions frontend/.env.local.example
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
# The APPLICATION_ID, XP_TOKEN_ID, and REWARD_TOKEN_ID are accessible by
# selecting "View Application Keys" on the Manage Application page in the OPENFORMAT App.

# This ID links your application with the OPENFORMAT platform.
NEXT_PUBLIC_APPLICATION_ID=

# Identifier for the Reward Token used in your application.
NEXT_PUBLIC_REWARD_TOKEN_ID=

# Wallet address of application owner. When a user spends a Reward Token, it will be sent to this address.
# This will generally be the same address you created the Application with in the OPENFORMAT App.
NEXT_PUBLIC_APPLICATION_OWNER_ADDRESS=

NEXT_PUBLIC_API_HOSTNAME=http://localhost:8080/api/v1
6 changes: 4 additions & 2 deletions frontend/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ export default function Footer() {
return (
<footer>
<nav>
<a href="/">OPENFORMAT App</a>
<a href="https://app.openformat.tech">OPENFORMAT Dashboard</a>
<a href="/game">GitHub</a>
<a href="/leaderboard">Community</a>
<a href="/quests">Docs</a>
<a className="created-with" href="https://openformat.tech">
Created with ❤️ by OPENFORMAT
</a>
</nav>
<p>Created with ❤️ by OPENFORMAT</p>
</footer>
);
}
8 changes: 5 additions & 3 deletions frontend/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function Header() {
};

const handleAuth = async () => {
if (!address) return;
try {
const challengeResponse = await apiClient.post(
"auth/challenge",
Expand All @@ -41,9 +42,10 @@ export default function Header() {
};

useEffect(() => {
const tokenExists = localStorage.getItem("tokens");
const tokens = localStorage.getItem("tokens");
const parsedTokens = JSON.parse(tokens);

if (address && !tokenExists) {
if ((address && !tokens) || address !== parsedTokens?.address) {
handleAuth();
}
}, [address]);
Expand All @@ -63,7 +65,7 @@ export default function Header() {
</div>
<nav>
<a href="/" className={isActive("/")}>
Play
App
</a>
<a href="/profile" className={isActive("/profile")}>
Profile
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/Quests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function Quests({
);

return (
<section id="achievements">
<section id="achievements" className="main">
<h2>{title}</h2>
<ul className="quests__list">
{quests && quests.length ? (
Expand Down
153 changes: 144 additions & 9 deletions frontend/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,164 @@
import Welcome from "@/components/Welcome";
import apiClient from "@/utils/apiClient";
import { useWallet } from "@openformat/react";
import { useBalance } from "@/utils/hooks";
import {
ContractType,
ERC20Base,
toWei,
useOpenFormat,
useWallet,
} from "@openformat/react";
import { BigNumber } from "ethers";
import { useState } from "react";

export default function PlayPage() {
const { address } = useWallet();
const [isLoading, setIsLoading] = useState<boolean>(false);
const { sdk } = useOpenFormat();

const [isLoadingRewards, setIsLoadingRewards] =
useState<boolean>(false);
const [isLoadingSpendToken, setIsLoadingSpendToken] =
useState<boolean>(false);

const [hasSpent, setHasSpent] = useState<boolean>(false);

const balance = useBalance(
process.env.NEXT_PUBLIC_REWARD_TOKEN_ID as string,
address as string
);

const BALANCE_TOO_LOW = BigNumber.from(balance).lte(1);

async function handleRewards() {
setIsLoading(true);
setIsLoadingRewards(true);
await apiClient
.post("rewards/token-system/trigger", {
user_address: address,
action_id: "test_action",
})
.then((res) =>
alert(`Success!, View transaction: ${res.data.transaction}`)
)
.catch((err) => console.log({ err }));
setIsLoading(false);
);
setIsLoadingRewards(false);
}

async function spendTokens() {
setIsLoadingSpendToken(true);
try {
if (!process.env.NEXT_PUBLIC_APPLICATION_OWNER_ADDRESS) {
throw new Error(
"NEXT_PUBLIC_APPLICATION_OWNER_ADDRESS not set in .env.local"
);
}
if (address) {
sdk.setStarId(
process.env.NEXT_PUBLIC_APPLICATION_ID as string
);
const rewardToken = (await sdk.getContract({
contractAddress: process.env
.NEXT_PUBLIC_REWARD_TOKEN_ID as string,
type: ContractType.Token,
})) as ERC20Base;

await rewardToken.transfer({
to: process.env
.NEXT_PUBLIC_APPLICATION_OWNER_ADDRESS as string,
amount: toWei("1"),
});

setHasSpent(true);
}
} catch (e: any) {
console.log(e.message);
alert(e.message);
}
setIsLoadingSpendToken(false);
}

return (
<section>
<Welcome handleRewards={handleRewards} isLoading={isLoading} />
<section id="welcome" className="main">
<h2>Demo Application</h2>
<div className="welcome__welcome">
<div className="welcome__description">
<p>
Here is our demo app. This basic application can reward
the connected user XP, Reward Tokens and Badges for
completing Actions and Quests.
</p>

<p>
To update the Actions and Quests (Missions) for this
application, please refer to the Token System Usage Guide
in the README.
</p>
</div>
</div>

<ul className="welcome__list">
<li className="welcome__item">
<h3 className="welcome__item-title">Reward Tokens</h3>
<p className="welcome__item-description">
This action sends XP and Reward Tokens to the connected
user. Ideal for incentivising user engagement. Connect
your wallet, press the Reward button, and watch your token
balance increase in your profile, and your address appear
in the leaderboard.
</p>
{address && (
<button
onClick={handleRewards}
disabled={isLoadingRewards}
>
{isLoadingRewards
? "Loading..."
: "Reward Connected User"}
</button>
)}
</li>
<li className="welcome__item">
<h3 className="welcome__item-title">Token-Based Access</h3>
<p className="welcome__item-description">
This action enables the connected user to spend Reward
Tokens to unlock the special content below. Ideal for
unlocking features or making in-app purchases. This action
could also allow access simply by holding a Token or
Badge. This is ideal for offering both active reward-based
interactions and passive benefits for user loyalty in your
application.
</p>
{address && (
<>
<button
onClick={spendTokens}
disabled={isLoadingSpendToken || BALANCE_TOO_LOW}
>
{isLoadingSpendToken
? "Loading..."
: "Spend 1 Token to Unlock Content"}
</button>
{BALANCE_TOO_LOW && (
<p className="welcome__item-error">
The connected wallet does not have enough Reward
Tokens. Trigger the action above to receive some
Reward Tokens.
</p>
)}
</>
)}
</li>

<li className="welcome__item">
<h3 className="welcome__item-title">Special Content 🔑</h3>
<p
className="welcome__item-description"
style={{
filter: !hasSpent ? "blur(8px)" : "blur(0px)",
}}
>
Some Special content that has been reveal once spending
Reward Tokens.
</p>
</li>
</ul>
</section>
);
}
11 changes: 9 additions & 2 deletions frontend/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ table {
--border-radius: 3px; /* Rounded corners */
--border-width: 1px; /* Thin borders */
--border-radius-circle: 100%;
--error-color: #ef4444;
}

/* Global styles */
Expand Down Expand Up @@ -190,6 +191,8 @@ header div {
}

.header__logo {
height: 20px;
width: 20px;
margin-right: 10px;
border-radius: 9999px;
}
Expand All @@ -212,8 +215,8 @@ nav a {
}

.header__logo {
height: 40px;
width: 40px;
height: 20px;
width: 20px;
}

.header__company {
Expand Down Expand Up @@ -495,6 +498,10 @@ footer {
padding: 1rem;
}

.welcome__item-error {
color: var(--error-color);
}

.welcome__description p {
margin-bottom: 1rem;
}
Expand Down
36 changes: 36 additions & 0 deletions frontend/utils/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
ContractType,
ERC20Base,
fromWei,
useOpenFormat,
} from "@openformat/react";
import { useEffect, useState } from "react";

export function useBalance(token: string, address: string) {
const { sdk } = useOpenFormat();
const [balance, setBalance] = useState(0);

useEffect(() => {
const fetchBalance = async () => {
if (!address) return;

try {
const rewardToken = (await sdk.getContract({
contractAddress: token,
type: ContractType.Token,
})) as ERC20Base;

const balance = await rewardToken.balanceOf({
account: address,
});
setBalance(Number(fromWei(balance.toString())));
} catch (error) {
console.error("Error fetching balance:", error);
}
};

fetchBalance();
}, [address]);

return balance;
}

0 comments on commit b897f2a

Please sign in to comment.