diff --git a/app/.env-example.env b/app/.env-example.env index 291ebfaac2..212efeb9dd 100644 --- a/app/.env-example.env +++ b/app/.env-example.env @@ -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 \ No newline at end of file +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 diff --git a/app/context/ceramicContext.tsx b/app/context/ceramicContext.tsx index 3f728c8b0b..3c12fa76a7 100644 --- a/app/context/ceramicContext.tsx +++ b/app/context/ceramicContext.tsx @@ -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, @@ -486,6 +487,7 @@ export const CeramicContextProvider = ({ children }: { children: any }) => { const [database, setDatabase] = useState(undefined); const { address, dbAccessToken, dbAccessTokenStatus } = useContext(UserContext); + const { submitPassport } = useContext(ScorerContext); useEffect(() => { return () => { @@ -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 }); @@ -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 }); diff --git a/app/context/scorerContext.tsx b/app/context/scorerContext.tsx new file mode 100644 index 0000000000..5b6f1cd90e --- /dev/null +++ b/app/context/scorerContext.tsx @@ -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; + submitPassport: (address: string | undefined) => Promise; +} + +const startingState: ScorerContextState = { + score: 0, + rawScore: 0, + threshold: 0, + scoreDescription: "", + passportSubmissionState: "APP_INITIAL", + scoreState: "APP_INITIAL", + refreshScore: async (address: string | undefined): Promise => {}, + submitPassport: async (address: string | undefined): Promise => {}, +}; + +// 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("APP_INITIAL"); + const [scoreState, setScoreState] = useState("APP_INITIAL"); + + const loadScore = async (address: string | undefined): Promise => { + 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 {children}; +}; diff --git a/app/pages/Dashboard.tsx b/app/pages/Dashboard.tsx index 77e85d5997..acde458d6a 100644 --- a/app/pages/Dashboard.tsx +++ b/app/pages/Dashboard.tsx @@ -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"; @@ -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(); @@ -60,6 +62,8 @@ export default function Dashboard() { useEffect(() => { if (!wallet) { navigate("/"); + } else { + refreshScore(wallet.accounts[0].address.toLowerCase()); } }, [wallet]); @@ -200,7 +204,7 @@ export default function Dashboard() { const subheader = useMemo( () => ( -
+
My {filterName && `${filterName} `}Stamps {filterName && ( @@ -218,6 +222,23 @@ export default function Dashboard() {
+
+
+ +
+
+
+ {/* TODO add color to theme */} + {rawScore.toFixed(2)} + + Your Unique Humanity Score is based out of 100 and measures how unique you are. The current passing + score threshold is 15. + +
+
{scoreDescription}
+
+
+
{passport ? (