Skip to content

Commit

Permalink
🚧
Browse files Browse the repository at this point in the history
  • Loading branch information
dieguezguille committed Nov 4, 2024
1 parent b2fb91d commit 122c2fb
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 106 deletions.
24 changes: 13 additions & 11 deletions server/api/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { integer, maxValue, minValue, number, parse, picklist, pipe, strictObjec
import database, { cards, credentials } from "../database";
import auth from "../middleware/auth";
import { createCard, getPAN } from "../utils/cryptomate";
import { getInquiry } from "../utils/persona";
import { getInquirires } from "../utils/persona";
import { track } from "../utils/segment";

const mutexes = new Map<string, Mutex>();
Expand Down Expand Up @@ -63,22 +63,24 @@ export default app
if (!credential) return c.json("credential not found", 401);
const account = parse(Address, credential.account);
setUser({ id: account });
if (!credential.kycId) return c.json("kyc required", 403);
const { data } = await getInquiry(credential.kycId);
if (data.attributes.status !== "approved") return c.json("kyc not approved", 403);
const { data: inquiries } = await getInquirires(credentialId);
console.log(inquiries); // TODO remove logs and comments
const inquiry = inquiries[0];
if (!inquiry) return c.json("kyc not found", 404);
if (inquiry.attributes.status !== "approved") return c.json("kyc not approved", 403);
if (credential.cards.length > 0) return c.json("card already exists", 400);
const phone = parsePhoneNumberWithError(
data.attributes["phone-number"].startsWith("+")
? data.attributes["phone-number"]
: `+${data.attributes["phone-number"]}`,
inquiry.attributes["phone-number"].startsWith("+")
? inquiry.attributes["phone-number"]
: `+${inquiry.attributes["phone-number"]}`,
);
const card = await createCard({
account,
email: data.attributes["email-address"],
email: inquiry.attributes["email-address"],
name: {
first: data.attributes["name-first"],
middle: data.attributes["name-middle"],
last: data.attributes["name-last"],
first: inquiry.attributes["name-first"],
middle: inquiry.attributes["name-middle"],
last: inquiry.attributes["name-last"],
},
phone: { countryCode: phone.countryCallingCode, number: phone.nationalNumber },
limits: { daily: 1000, weekly: 3000, monthly: 5000 },
Expand Down
87 changes: 35 additions & 52 deletions server/api/kyc.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Address } from "@exactly/common/validation";
import { vValidator } from "@hono/valibot-validator";
import { setUser } from "@sentry/node";
import { eq } from "drizzle-orm";
import { Hono } from "hono";
import { object, optional, parse, string } from "valibot";
import { parse } from "valibot";

import database, { credentials } from "../database/index";
import auth from "../middleware/auth";
import { createInquiry, generateOTL, getInquiry } from "../utils/persona";
import { createInquiry, generateOTL, getInquirires, getSessionToken } from "../utils/persona";

const app = new Hono();
app.use(auth);
Expand All @@ -21,55 +20,39 @@ export default app
});
if (!credential) return c.json("credential not found", 404);
setUser({ id: parse(Address, credential.account) });
if (!credential.kycId) return c.json("kyc not found", 404);
const { data } = await getInquiry(credential.kycId);
if (data.attributes.status !== "approved") return c.json("kyc not approved", 403);
const { data: inquiries } = await getInquirires(credentialId);
console.log(inquiries); // TODO remove logs and comments
if (!inquiries[0]) return c.json("kyc not found", 404);
if (
inquiries[0].attributes.status === "created" ||
inquiries[0].attributes.status === "pending" ||
inquiries[0].attributes.status === "expired"
) {
const { meta } = await getSessionToken(inquiries[0].id);
return c.json({ inquiryId: inquiries[0].id, sessionToken: meta["session-token"] }, 200);
}
if (inquiries[0].attributes.status !== "approved") return c.json("kyc not approved", 400);
return c.json("ok", 200);
})
.post(
"/",
vValidator("json", object({ inquiryId: optional(string()) }), ({ success }, c) => {
if (!success) return c.json("invalid body", 400);
}),
async (c) => {
const credentialId = c.get("credentialId");
const credential = await database.query.credentials.findFirst({
columns: { id: true, account: true, kycId: true },
where: eq(credentials.id, credentialId),
});
if (!credential) return c.json("credential not found", 404);
setUser({ id: parse(Address, credential.account) });
const { inquiryId } = c.req.valid("json");
if (inquiryId) {
const { data } = await getInquiry(inquiryId);
if (data.attributes["reference-id"] !== credentialId) return c.json("unauthorized", 403);
if (credential.kycId !== data.id) {
await database.update(credentials).set({ kycId: data.id }).where(eq(credentials.id, credentialId));
}
return c.json("ok", 200);
}
if (credential.kycId) {
const { data } = await getInquiry(credential.kycId);
if (data.attributes["reference-id"] !== credentialId) return c.json("unauthorized", 403);
switch (data.attributes.status) {
case "created": {
const { meta } = await generateOTL(credential.kycId);
return c.json(meta["one-time-link"]);
}
case "expired": {
const { data: inquiry } = await createInquiry(credentialId);
const { meta } = await generateOTL(inquiry.id);
return c.json(meta["one-time-link"]);
}
case "approved":
return c.json("ok", 200);
default:
return c.json("kyc not approved", 403);
}
.post("/", async (c) => {
const credentialId = c.get("credentialId");
const credential = await database.query.credentials.findFirst({
columns: { id: true, account: true, kycId: true },
where: eq(credentials.id, credentialId),
});
if (!credential) return c.json("credential not found", 404);
setUser({ id: parse(Address, credential.account) });
const { data: inquiries } = await getInquirires(credentialId);
console.log(inquiries); // TODO remove logs and comments
if (inquiries[0]) {
if (inquiries[0].attributes.status === "approved") return c.json("kyc already approved", 400);
if (inquiries[0].attributes.status === "created" || inquiries[0].attributes.status === "expired") {
const { meta } = await generateOTL(inquiries[0].id);
return c.json(meta["one-time-link"]);
}
const { data } = await createInquiry(credentialId); // TODO check for existing persona inquiries
const { meta } = await generateOTL(data.id);
await database.update(credentials).set({ kycId: data.id }).where(eq(credentials.id, credentialId));
return c.json(meta["one-time-link"]);
},
);
return c.json("kyc failed", 400);
}
const { data } = await createInquiry(credentialId);
const { meta } = await generateOTL(data.id);
return c.json(meta["one-time-link"]);
});
64 changes: 54 additions & 10 deletions server/utils/persona.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { type BaseIssue, type BaseSchema, literal, nullable, object, parse, picklist, string, variant } from "valibot";
import {
array,
type BaseIssue,
type BaseSchema,
literal,
nullable,
object,
parse,
picklist,
string,
variant,
} from "valibot";

import appOrigin from "./appOrigin";

Expand All @@ -10,8 +21,12 @@ const authorization = `Bearer ${process.env.PERSONA_API_KEY}`;
const templateId = process.env.PERSONA_TEMPLATE_ID;
const baseURL = process.env.PERSONA_URL;

export function getInquiry(inquiryId: string) {
return request(GetInquiryResponse, `/inquiries/${inquiryId}`);
export function getInquirires(referenceId: string) {
return request(GetInquiriesResponse, `/inquiries?filter[reference-id]=${referenceId}`);
}

export function getSessionToken(inquiryId: string) {
return request(GetSessionTokenResponse, `/inquiries/${inquiryId}/resume`);
}

export function createInquiry(referenceId: string) {
Expand Down Expand Up @@ -40,14 +55,35 @@ async function request<TInput, TOutput, TIssue extends BaseIssue<unknown>>(
return parse(schema, await response.json());
}

const CreateInquiryResponse = object({
data: object({
id: string(),
type: literal("inquiry"),
attributes: object({ status: literal("created"), "reference-id": string() }),
}),
const GetInquiriesResponse = object({
data: array(
object({
id: string(),
type: literal("inquiry"),
attributes: variant("status", [
object({
status: picklist(["completed", "approved"]),
"reference-id": string(),
"name-first": string(),
"name-middle": nullable(string()),
"name-last": string(),
"email-address": string(),
"phone-number": string(),
}),
object({
status: picklist(["created", "pending", "expired", "failed", "needs_review", "declined"]),
"reference-id": string(),
"name-first": nullable(string()),
"name-middle": nullable(string()),
"name-last": nullable(string()),
"email-address": nullable(string()),
"phone-number": nullable(string()),
}),
]),
}),
),
});
const GetInquiryResponse = object({
const GetSessionTokenResponse = object({
data: object({
id: string(),
type: literal("inquiry"),
Expand All @@ -72,6 +108,14 @@ const GetInquiryResponse = object({
}),
]),
}),
meta: object({ "session-token": string() }),
});
const CreateInquiryResponse = object({
data: object({
id: string(),
type: literal("inquiry"),
attributes: object({ status: literal("created"), "reference-id": string() }),
}),
});
const GenerateOTLResponse = object({
data: object({
Expand Down
39 changes: 25 additions & 14 deletions src/components/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
useReadUpgradeableModularAccountGetInstalledPlugins,
} from "../../generated/contracts";
import handleError from "../../utils/handleError";
import { verifyIdentity } from "../../utils/persona";
import { createInquiry, resumeInquiry } from "../../utils/persona";
import queryClient from "../../utils/queryClient";
import { APIError, getActivity, getCard, createCard, kycStatus } from "../../utils/server";
import { APIError, getActivity, getCard, createCard, getKYCStatus } from "../../utils/server";
import useIntercom from "../../utils/useIntercom";
import useMarketAccount from "../../utils/useMarketAccount";
import InfoBadge from "../shared/InfoBadge";
Expand Down Expand Up @@ -51,7 +51,10 @@ export default function Card() {

const { queryKey } = useMarketAccount(marketUSDCAddress);
const { address } = useAccount();
const { data: KYCStatus, refetch: refetchKYCStatus } = useQuery({ queryKey: ["kyc", "status"], queryFn: kycStatus });
const { data: KYCStatus, refetch: refetchKYCStatus } = useQuery({
queryKey: ["kyc", "status"],
queryFn: getKYCStatus,
});
const { refetch: refetchInstalledPlugins } = useReadUpgradeableModularAccountGetInstalledPlugins({
address: address ?? zeroAddress,
});
Expand Down Expand Up @@ -86,31 +89,39 @@ export default function Card() {
} = useMutation({
mutationKey: ["card", "reveal"],
mutationFn: async function handleReveal() {
if (!passkey || isRevealing) return;
if (!isRevealing) return;
if (!passkey) return;
try {
// if card exists, open details
const { isSuccess, data } = await refetchCard();
if (isSuccess && data.url) {
setCardDetailsOpen(true);
return;
}
await kycStatus();
await createCard();
const { data: card } = await refetchCard();
if (card?.url) setCardDetailsOpen(true);
// if card doesn't exist, check the KYC status
const result = await getKYCStatus();
if (result === "ok") {
// if KYC is ok, create a card and open details
await createCard();
const { data: card } = await refetchCard();
if (card?.url) setCardDetailsOpen(true);
} else {
// if KYC is able to resume, resume existing inquiry
resumeInquiry(result.inquiryId, result.sessionToken);
}
} catch (error) {
// if unknown error, report to sentry and return
if (!(error instanceof APIError)) {
handleError(error);
return;
}
// handle expected kyc errors
const { code, text } = error;
// if no kyc, create a new inquiry
if ((code === 403 && text === "kyc required") || (code === 404 && text === "kyc not found")) {
await verifyIdentity(passkey);
}
if (code === 404 && text === "card not found") {
await createCard();
const { data: card } = await refetchCard();
if (card?.url) setCardDetailsOpen(true);
createInquiry(passkey);
}
// report unexpected errors
handleError(error);
}
},
Expand Down
7 changes: 5 additions & 2 deletions src/components/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import GettingStarted from "./GettingStarted";
import HomeActions from "./HomeActions";
import { useReadPreviewerExactly } from "../../generated/contracts";
import handleError from "../../utils/handleError";
import { getActivity, kycStatus } from "../../utils/server";
import { getActivity, getKYCStatus } from "../../utils/server";
import AlertBadge from "../shared/AlertBadge";
import LatestActivity from "../shared/LatestActivity";
import ProfileHeader from "../shared/ProfileHeader";
Expand All @@ -38,7 +38,10 @@ export default function Home() {
account: address,
args: [address ?? zeroAddress],
});
const { data: KYCStatus, refetch: refetchKYCStatus } = useQuery({ queryKey: ["kyc", "status"], queryFn: kycStatus });
const { data: KYCStatus, refetch: refetchKYCStatus } = useQuery({
queryKey: ["kyc", "status"],
queryFn: getKYCStatus,
});
let usdBalance = 0n;
if (markets) {
for (const market of markets) {
Expand Down
30 changes: 16 additions & 14 deletions src/utils/persona.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import type { Passkey } from "@exactly/common/validation";
import { Platform } from "react-native";
import { Environment, Inquiry } from "react-native-persona";

import handleError from "./handleError";
import queryClient from "./queryClient";
import { kyc } from "./server";

export const templateId = "itmpl_8uim4FvD5P3kFpKHX37CW817";
export const environment = __DEV__ ? Environment.SANDBOX : Environment.PRODUCTION;

export async function verifyIdentity(passkey: Passkey) {
if (Platform.OS === "web") {
const otl = await kyc();
window.open(otl, "_self");
return;
}
export function createInquiry(passkey: Passkey) {
Inquiry.fromTemplate(templateId)
.environment(environment)
.referenceId(passkey.credentialId)
.onCanceled((inquiryId) => {
if (!inquiryId) throw new Error("no inquiry id");
kyc(inquiryId).catch(handleError);
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(handleError);
})
.onComplete((inquiryId) => {
if (!inquiryId) throw new Error("no inquiry id");
kyc(inquiryId).catch(handleError);
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(handleError);
})
.onError(handleError)
.build()
.start();
}

export function resumeInquiry(inquiryId: string, sessionToken: string) {
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(handleError);
})
.onComplete(() => {
queryClient.invalidateQueries({ queryKey: ["kyc", "status"] }).catch(handleError);
})
.build()
.start();
}
Loading

0 comments on commit 122c2fb

Please sign in to comment.