Skip to content

Commit

Permalink
Add three sample apps (passportxyz#214)
Browse files Browse the repository at this point in the history
* Add three sample apps

* Add tooltip

* Update denied and dashboard styles

* Update AirDrop styles

* Add admin address table

* Deploy

* Fix quotes

* Fix quotes again

* Add ability for admin to manual add to airdrop

* Fix admin add

* Remove console logs

* Consolidate env files and remove console logs

* Add info cards to admin dashboard

* Add pagination

* Style pagination

* Add configure theme

* Commit to rebuild

* Add keys

* Replace a with Link

* Allow for admin theme customization

* Style configure theme page

* Style configure link

* Add knex

* Change knex configuration

* Add ssl

* Add env vars to knexfile

* Fix getServerSideProps for admin dashboard
  • Loading branch information
Rask467 authored May 8, 2023
1 parent 956c215 commit da34d61
Show file tree
Hide file tree
Showing 94 changed files with 30,099 additions and 0 deletions.
3 changes: 3 additions & 0 deletions examples/airdrop/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
36 changes: 36 additions & 0 deletions examples/airdrop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

.env

dev.sqlite3
54 changes: 54 additions & 0 deletions examples/airdrop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# AirDrop

## Introduction

This sample app connects to a user's wallet, then fetches their passport score from the passport scorer API and uses it to determine if they are allowed to claim an airdrop.

## Getting Started

### Create your API key and Scorer

1. Create your API key by going to [Gitcoin Passport Scorer](https://scorer.gitcoin.co) and clicking on the "API Keys" section.
Then create a `.env.local` file and copy the contents of the `example.local.env` file into it.
Replace `SCORER_API_KEY` with your API key.

![Create api key](./screenshots/crete_api_key.png)

2. Create a Scorer, by clicking on the "Scorer" section.
Replace `NEXT_PUBLIC_SCORER_ID` with your Scorer ID in `.env.local`.

![Create scorer](./screenshots/create_scorer.png)

3. Run migrations `npx knex migrate:latest`

### Start the app

Now you can start the app by running:

```bash
cd examples/airdrop && npm install
```

then

```bash
npm run dev
```

Finally, you can navigate to `http://localhost:3000` to view the sample app.

Navigate to `http://localhost:3000/admin/dashboard` to view the admin dashboard.

### Layout

Most of the logic for connecting to the passport scorer API can be found in `/components/AirDrop.js`

Inside this component we:

1. Fetch a message and nonce from the scorer API.
2. Prompt the user to sign the message.
3. Submit the user's passport for scoring.
4. Fetch the user's passport score.
5. Use the score to determine if they are eligible for the airdrop.

All of the interaction with the scorer API is proxied through our endpoints in `/pages/api`. This is done to prevent our `SCORER_API_KEY` env variable from being exposed to the frontend.
38 changes: 38 additions & 0 deletions examples/airdrop/components/AddAirdrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import styles from "@/styles/Dashboard.module.css";
import { useState } from "react";

export default function AddAirdrop({ cancel, add }) {
const [address, setAddress] = useState("");

return (
<div
style={{
marginTop: "21px",
display: "flex",
justifyContent: "flex-end",
}}
>
<input
onChange={(e) => setAddress(e.target.value)}
style={{
padding: "7px 5px",
borderRadius: "10px",
marginRight: "10px",
}}
placeholder="0x..."
type="text"
/>
<button onClick={() => add(address)} className={styles.btn} type="submit">
Add
</button>
<button
onClick={cancel}
style={{ marginLeft: "10px" }}
className={styles.btn}
type="button"
>
Cancel
</button>
</div>
);
}
186 changes: 186 additions & 0 deletions examples/airdrop/components/AirDrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { useState, useEffect } from "react";
import axios from "axios";
import { verifyMessage } from "ethers/lib/utils";
import { useAccount, useSignMessage } from "wagmi";
import styles from "@/styles/AirDrop.module.css";

export default function AirDrop() {
useEffect(() => {
setIsMounted(true);
}, []);

const { address } = useAccount({
onDisconnect() {
reset();
},
});

useEffect(() => {
reset();
// Query the airdrop_addresses table to check if this user is already in the aidrop list.
// If they are there is no need to connect to the scorer API, we can display their passport score immediately.
async function checkIfAddressAlreadyInAirdropList() {
const resp = await axios.post(`/api/airdrop/check/${address}`);
if (resp?.data?.address) {
setChecked(true);
setPassportScore(resp.data.score);
}
}
checkIfAddressAlreadyInAirdropList();
}, [address]);

async function addToAirdrop() {
setNonce("");
setPassportScore(0);
// Step #1 (Optional, only required if using the "signature" param when submitting a user's passport. See https://docs.passport.gitcoin.co/building-with-passport/scorer-api/endpoint-definition#submit-passport)
// We call our /api/scorer-message endpoint (/pages/api/scorer-message.js) which internally calls /registry/signing-message
// on the scorer API. Instead of calling /registry/signing-message directly, we call it via our api endpoint so we do not
// expose our scorer API key to the frontend.
// This will return a response like:
// {
// message: "I hereby agree to submit my address in order to score my associated Gitcoin Passport from Ceramic.",
// nonce: "b7e3b0f86820744b9242dd99ce91465f10c961d98aa9b3f417f966186551"
// }
const scorerMessageResponse = await axios.get("/api/scorer-message");
if (scorerMessageResponse.status !== 200) {
console.error("failed to fetch scorer message");
return;
}
setNonce(scorerMessageResponse.data.nonce);

// Step #2 (Optional, only required if using the "signature" param when submitting a user's passport.)
// Have the user sign the message that was returned from the scorer api in Step #1.
signMessage({ message: scorerMessageResponse.data.message });
}

const { signMessage } = useSignMessage({
async onSuccess(data, variables) {
// Verify signature when sign message succeeds
const address = verifyMessage(variables.message, data);

// Step #3
// Now that we have the signature from the user, we can submit their passport for scoring
// We call our /api/submit-passport endpoint (/pages/api/submit-passport.js) which
// internally calls /registry/submit-passport on the scorer API.
// This will return a response like:
// {
// address: "0xabc",
// error: null,
// evidence: null,
// last_score_timestamp: "2023-03-26T15:17:03.393567+00:00",
// score: null,
// status: "PROCESSING"
// }
const submitResponse = await axios.post("/api/submit-passport", {
address: address, // Required: The user's address you'd like to score.
community: process.env.NEXT_PUBLIC_SCORER_ID, // Required: get this from one of your scorers in the Scorer API dashboard https://scorer.gitcoin.co/
signature: data, // Optional: The signature of the message returned in Step #1
nonce: nonce, // Optional: The nonce returned in Step #1
});

// Step #4
// Finally, we can attempt to add the user to the airdrop list.
// We call our /api/airdrop/{scorer_id}/{address} endpoint (/pages/api/airdrop/[scorer_id]/[address].js) which internally calls
// /registry/score/{scorer_id}/{address}
// This will return a response like:
// {
// address: "0xabc",
// error: null,
// evidence: null,
// last_score_timestamp: "2023-03-26T15:17:03.393567+00:00",
// score: "1.574606692",
// status: ""DONE""
// }
// We check if the returned score is above our threshold to qualify for the airdrop.
// If the score is above our threshold, we add the user to the airdrop list.
const scoreResponse = await axios.get(
`/api/airdrop/add/${process.env.NEXT_PUBLIC_SCORER_ID}/${address}`
);

// Make sure to check the status
if (scoreResponse.data.status === "ERROR") {
setPassportScore(0);
alert(scoreResponse.data.error);
return;
}

// Store the user's passport score for later use.
setPassportScore(scoreResponse.data.score);
setChecked(true);

if (scoreResponse.data.score < 1) {
alert("Sorry, you're not eligible for the airdrop.");
} else {
alert("You're eligible for the airdrop! Your address has been added.");
}
},
});

function reset() {
setNonce("");
setPassportScore(0);
setChecked(false);
}

// This isMounted check is needed to prevent hydration errors with next.js server side rendering.
// See https://github.com/wagmi-dev/wagmi/issues/542 for more details.
const [isMounted, setIsMounted] = useState(false);
const [nonce, setNonce] = useState("");
const [passportScore, setPassportScore] = useState(0);
const [checked, setChecked] = useState(false);

function display() {
if (isMounted && address) {
if (checked) {
if (passportScore < 1) {
return (
<div>
<p className={styles.p}>
Your score isn&apos;t high enough, collect more stamps to
qualify.
</p>
<p style={{ marginTop: "20px" }} className={styles.p}>
Passport Score:{" "}
<span style={{ color: "rgb(111 63 245" }}>
{passportScore | 0}
</span>
/1
</p>
<div style={{ marginTop: "10px" }}>
<a
className={styles.link}
target="_blank"
rel="noreferrer"
href="https://passport.gitcoin.co"
>
Click here to increase your score.
</a>
</div>
</div>
);
} else {
return (
<div>
<p className={styles.p}>You&apos;ve been added to the airdrop!</p>
</div>
);
}
} else {
return (
<button className={styles.btn} onClick={() => addToAirdrop(address)}>
Add address to airdrop
</button>
);
}
} else {
return (
<p className={styles.p}>
Connect your wallet to find out if you&apos;re eligible for the
airdrop.
</p>
);
}
}

return <div>{display()}</div>;
}
16 changes: 16 additions & 0 deletions examples/airdrop/components/InfoCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "@/styles/InfoCard.module.css";

export default function InfoCard({ title, icon, value }) {
return (
<div className={styles.card}>
<div className={styles.header}>
<h3 className={styles.title}>{title}</h3>
<div className={styles.iconContainer}>
<FontAwesomeIcon color="rgb(108, 122, 137)" icon={icon} />
</div>
</div>
<p className={styles.value}>{value}</p>
</div>
);
}
Loading

0 comments on commit da34d61

Please sign in to comment.