Skip to content

Commit

Permalink
feat(app): adding scorer context (passportxyz#1166)
Browse files Browse the repository at this point in the history
* feat(app): adding scorer context

* feat(app): trying to load score from Passport Dashboard

* feat(app): adds context for passport submission

* feat(app): implementing passpor submission, score refresh, and displaying it in the dashboard

* feat(app): polishing score display in passport

- also added automatic submission when no passport or initial score

* flex positioning for score

* aligning score spinner, fixing submit-passport timing

---------

Co-authored-by: Aminah Burch <[email protected]>
  • Loading branch information
nutrina and aminah-io authored Apr 21, 2023
1 parent 91e1f2c commit 52e52d7
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 14 deletions.
6 changes: 5 additions & 1 deletion app/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ NEXT_PUBLIC_FF_ONE_CLICK_VERIFICATION=on

NEXT_PUBLIC_CERAMIC_CACHE_ENDPOINT=http://localhost:8002/

NEXT_PUBLIC_INTERCOM_APP_ID=YOUR_INTERCOM_APP_ID
NEXT_PUBLIC_INTERCOM_APP_ID=YOUR_INTERCOM_APP_ID

NEXT_PUBLIC_ALLO_SCORER_ID=ALLO_SCORER_ID
NEXT_PUBLIC_ALLO_SCORER_API_KEY=SCORER_API_KEY
NEXT_PUBLIC_SCORER_ENDPOINT=http://localhost:8002/registry
4 changes: 4 additions & 0 deletions app/context/ceramicContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useViewerConnection } from "@self.id/framework";
import { datadogLogs } from "@datadog/browser-logs";
import { datadogRum } from "@datadog/browser-rum";
import { UserContext } from "./userContext";
import { ScorerContext } from "./scorerContext";
import {
Twitter,
Ens,
Expand Down Expand Up @@ -486,6 +487,7 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
const [database, setDatabase] = useState<PassportDatabase | undefined>(undefined);

const { address, dbAccessToken, dbAccessTokenStatus } = useContext(UserContext);
const { submitPassport } = useContext(ScorerContext);

useEffect(() => {
return () => {
Expand Down Expand Up @@ -758,6 +760,7 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
if (ceramicClient && newPassport) {
ceramicClient.setStamps(newPassport.stamps);
}
submitPassport(address);
}
} catch (e) {
datadogLogs.logger.error("Error adding multiple stamps", { stamps, error: e });
Expand All @@ -774,6 +777,7 @@ export const CeramicContextProvider = ({ children }: { children: any }) => {
if (ceramicClient && newPassport) {
ceramicClient.setStamps(newPassport.stamps);
}
submitPassport(address);
}
} catch (e) {
datadogLogs.logger.error("Error deleting multiple stamps", { providerIds, error: e });
Expand Down
142 changes: 142 additions & 0 deletions app/context/scorerContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// --- React Methods
import React, { createContext, useState } from "react";

// --- Axios
import axios, { AxiosError } from "axios";

const scorerId = process.env.NEXT_PUBLIC_ALLO_SCORER_ID;
const scorerApiKey = process.env.NEXT_PUBLIC_ALLO_SCORER_API_KEY || "";
const scorerApiSubmitPassport = process.env.NEXT_PUBLIC_SCORER_ENDPOINT + "/submit-passport";
const scorerApiGetScore = process.env.NEXT_PUBLIC_SCORER_ENDPOINT + "/score";

export type PassportSubmissionStateType =
| "APP_INITIAL"
| "APP_REQUEST_PENDING"
| "APP_REQUEST_ERROR"
| "APP_REQUEST_SUCCESS";
export type ScoreStateType = "APP_INITIAL" | "PROCESSING" | "ERROR" | "DONE";

export interface ScorerContextState {
score: number;
rawScore: number;
threshold: number;
scoreDescription: string;
passportSubmissionState: PassportSubmissionStateType;
scoreState: ScoreStateType;

refreshScore: (address: string | undefined) => Promise<void>;
submitPassport: (address: string | undefined) => Promise<void>;
}

const startingState: ScorerContextState = {
score: 0,
rawScore: 0,
threshold: 0,
scoreDescription: "",
passportSubmissionState: "APP_INITIAL",
scoreState: "APP_INITIAL",
refreshScore: async (address: string | undefined): Promise<void> => {},
submitPassport: async (address: string | undefined): Promise<void> => {},
};

// create our app context
export const ScorerContext = createContext(startingState);

export const ScorerContextProvider = ({ children }: { children: any }) => {
const [score, setScore] = useState(0);
const [rawScore, setRawScore] = useState(0);
const [threshold, setThreshold] = useState(0);
const [scoreDescription, setScoreDescription] = useState("");
const [passportSubmissionState, setPassportSubmissionState] = useState<PassportSubmissionStateType>("APP_INITIAL");
const [scoreState, setScoreState] = useState<ScoreStateType>("APP_INITIAL");

const loadScore = async (address: string | undefined): Promise<string> => {
try {
setScoreState("APP_INITIAL");
const response = await axios.get(`${scorerApiGetScore}/${scorerId}/${address}`, {
headers: {
"X-API-Key": scorerApiKey,
},
});
setScoreState(response.data.status);
if (response.data.status === "DONE") {
setScore(response.data.score);

const numRawScore = Number.parseFloat(response.data.evidence.rawScore);
const numThreshold = Number.parseFloat(response.data.evidence.threshold);
const numScore = Number.parseFloat(response.data.score);

setRawScore(numRawScore);
setThreshold(numThreshold);
setScore(numScore);

if (numRawScore > numThreshold) {
setScoreDescription("Passing Score");
} else {
setScoreDescription("Low Score");
}
}

return response.data.status;
} catch (error) {
throw error;
}
};

const refreshScore = async (address: string | undefined, submitPassportOnFailure: boolean = true) => {
if (address) {
setPassportSubmissionState("APP_REQUEST_PENDING");
try {
let scoreStatus = await loadScore(address);

while (scoreStatus === "PROCESSING") {
await new Promise((resolve) => setTimeout(resolve, 3000));
scoreStatus = await loadScore(address);
}
setPassportSubmissionState("APP_REQUEST_SUCCESS");
} catch (error: AxiosError | any) {
setPassportSubmissionState("APP_REQUEST_ERROR");
if (submitPassportOnFailure && error.response.data.detail === "Unable to get score for provided scorer.") {
submitPassport(address);
}
}
}
};

const submitPassport = async (address: string | undefined) => {
if (address) {
try {
await axios.post(
scorerApiSubmitPassport,
{
address,
scorer_id: scorerId,
},
{
headers: {
"X-API-Key": scorerApiKey,
},
}
);
// Refresh score, but set the submitPassportOnFailure to false -> we want to avoid a loop
refreshScore(address, false);
} catch (error) {
console.error(error);
}
}
};

// use props as a way to pass configuration values
const providerProps = {
score,
rawScore,
threshold,
scoreDescription,
passportSubmissionState,
scoreState,
refreshScore,
submitPassport,
};

return <ScorerContext.Provider value={providerProps}>{children}</ScorerContext.Provider>;
};
25 changes: 23 additions & 2 deletions app/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {

import { CeramicContext, IsLoadingPassportState } from "../context/ceramicContext";
import { UserContext } from "../context/userContext";
import { ScorerContext } from "../context/scorerContext";

import { useViewerConnection } from "@self.id/framework";
import { EthereumAuthProvider } from "@self.id/web";
Expand All @@ -40,6 +41,7 @@ export default function Dashboard() {
const { passport, isLoadingPassport, passportHasCacaoError, cancelCeramicConnection, expiredProviders } =
useContext(CeramicContext);
const { wallet, toggleConnection, userWarning, setUserWarning } = useContext(UserContext);
const { score, rawScore, refreshScore, scoreDescription, passportSubmissionState } = useContext(ScorerContext);

const { isOpen, onOpen, onClose } = useDisclosure();

Expand All @@ -60,6 +62,8 @@ export default function Dashboard() {
useEffect(() => {
if (!wallet) {
navigate("/");
} else {
refreshScore(wallet.accounts[0].address.toLowerCase());
}
}, [wallet]);

Expand Down Expand Up @@ -200,7 +204,7 @@ export default function Dashboard() {
const subheader = useMemo(
() => (
<PageWidthGrid nested={true} className="my-4">
<div className="col-span-3 flex items-center justify-items-center self-center lg:col-span-4">
<div className="col-span-2 flex items-center justify-items-center self-center lg:col-span-4">
<div className="flex text-2xl">
My {filterName && `${filterName} `}Stamps
{filterName && (
Expand All @@ -218,6 +222,23 @@ export default function Dashboard() {
</div>
</div>

<div className="col-span-1 col-end-[-2] flex min-w-fit items-center justify-self-end">
<div className={`pr-2 ${passportSubmissionState === "APP_REQUEST_PENDING" ? "visible" : "invisible"}`}>
<Spinner className="my-[2px]" thickness="2px" speed="0.65s" emptyColor="darkGray" color="gray" size="md" />
</div>
<div className="flex flex-col items-center">
<div className="flex text-2xl">
{/* TODO add color to theme */}
<span className={`${score == 1 ? "text-accent-3" : "text-[#FFE28A]"}`}>{rawScore.toFixed(2)}</span>
<Tooltip>
Your Unique Humanity Score is based out of 100 and measures how unique you are. The current passing
score threshold is 15.
</Tooltip>
</div>
<div className="flex whitespace-nowrap text-sm">{scoreDescription}</div>
</div>
</div>

<div className="col-span-1 col-end-[-1] justify-self-end">
{passport ? (
<button
Expand Down Expand Up @@ -245,7 +266,7 @@ export default function Dashboard() {
</div>
</PageWidthGrid>
),
[filterName, onOpen, passport]
[filterName, onOpen, passport, score, rawScore, scoreDescription, passportSubmissionState]
);

return (
Expand Down
25 changes: 14 additions & 11 deletions app/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Head from "next/head";
import "../styles/globals.css";
import { UserContextProvider } from "../context/userContext";
import { CeramicContextProvider } from "../context/ceramicContext";
import { ScorerContextProvider } from "../context/scorerContext";
import ManageAccountCenter from "../components/ManageAccountCenter";

// --- Ceramic Tools
Expand Down Expand Up @@ -154,17 +155,19 @@ function App({ Component, pageProps }: AppProps) {
session={true}
>
<UserContextProvider>
<CeramicContextProvider>
<ManageAccountCenter>
<div className="font-body" suppressHydrationWarning>
{typeof window === "undefined" ? null : (
<ThemeWrapper initChakra={true} defaultTheme={themes.LUNARPUNK_DARK_MODE}>
<Component {...pageProps} />
</ThemeWrapper>
)}
</div>
</ManageAccountCenter>
</CeramicContextProvider>
<ScorerContextProvider>
<CeramicContextProvider>
<ManageAccountCenter>
<div className="font-body" suppressHydrationWarning>
{typeof window === "undefined" ? null : (
<ThemeWrapper initChakra={true} defaultTheme={themes.LUNARPUNK_DARK_MODE}>
<Component {...pageProps} />
</ThemeWrapper>
)}
</div>
</ManageAccountCenter>
</CeramicContextProvider>
</ScorerContextProvider>
</UserContextProvider>
</SelfIdProvider>
</>
Expand Down

0 comments on commit 52e52d7

Please sign in to comment.