Skip to content

Commit

Permalink
wip search for usernames credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
peterferguson committed Mar 16, 2024
1 parent efa4142 commit 3667dee
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 89 deletions.
115 changes: 52 additions & 63 deletions apps/dapp/src/components/create-payment-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
import { getPaymentOrigin, payWithSPC } from "@/lib/utils";
import { getPaymentDetails, getPaymentOrigin, payWithSPC } from "@/lib/utils";
import { getAllowedCredentialsSchema } from "helpers";

const getIframe = () => {
Expand All @@ -26,56 +26,63 @@ const fallbackToIframeCredentialCreation = async () => {
};

/**
* Get the available credentials from the wallet iframe
* A utility function to wait for a message from the wallet iframe
*/
const getAvailableCredentials = async () => {
const createMessagePromise = (eventType: string): Promise<string[]> => {
const iframe = getIframe();

if (!iframe) throw new Error("No iframe found");

const iframeOrigin = iframe?.src ? new URL(iframe.src).origin : undefined;

// - Create a Promise that resolves when the expected message is received
const createCredentialsPromise = (): Promise<string[]> => {
let timeout: NodeJS.Timeout | undefined;

const credentialsPromise: Promise<string[]> = new Promise(
(resolve, reject) => {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function handleMessage(event: any) {
const eventOrigin = new URL(event.origin).origin;
if (eventOrigin !== iframeOrigin) return;

console.log("received message", event);

switch (event.data.type) {
case "credentials.get": {
const parsedMessage = getAllowedCredentialsSchema.parse(
event.data,
);

resolve(parsedMessage.credentials);
}
}
// Remove the event listener to clean up
window.removeEventListener("message", handleMessage);
let timeout: NodeJS.Timeout | undefined;
const credentialsPromise: Promise<string[]> = new Promise(
(resolve, reject) => {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function handleMessage(event: any) {
const eventOrigin = new URL(event.origin).origin;
if (eventOrigin !== iframeOrigin) return;

console.log("received message", event);

if (event.data.type === eventType) {
resolve(event.data);
}
// Remove the event listener to clean up
window.removeEventListener("message", handleMessage);
}

// Add an event listener to listen for messages from the iframe
window.addEventListener("message", handleMessage, false);

// Set a timeout to reject the promise if no response is received within a specific timeframe
timeout = setTimeout(() => {
window.removeEventListener("message", handleMessage);
reject(new Error("Timeout waiting for credentials response"));
}, 10000); // 10 seconds timeout
},
);

// Add an event listener to listen for messages from the iframe
window.addEventListener("message", handleMessage, false);
// ! Ensure to clean up on promise resolution or rejection
credentialsPromise.finally(() => clearTimeout(timeout));

// Set a timeout to reject the promise if no response is received within a specific timeframe
timeout = setTimeout(() => {
window.removeEventListener("message", handleMessage);
reject(new Error("Timeout waiting for credentials response"));
}, 10000); // 10 seconds timeout
},
);
return credentialsPromise;
};

// ! Ensure to clean up on promise resolution or rejection
credentialsPromise.finally(() => clearTimeout(timeout));
/**
* Get the available credentials from the wallet iframe
*/
const getAvailableCredentials = async () => {
const iframe = getIframe();

if (!iframe) throw new Error("No iframe found");

return credentialsPromise;
// - Create a Promise that resolves when the expected message is received from the wallet iframe
const waitForCredentials = async () => {
const eventData = await createMessagePromise("credentials.get");
const parsedMessage = getAllowedCredentialsSchema.parse(eventData);

return parsedMessage.credentials;
};

// - Ask the iframe for the available credentials
Expand All @@ -84,17 +91,20 @@ const getAvailableCredentials = async () => {
getPaymentOrigin(),
);

return await createCredentialsPromise();
return await waitForCredentials();
};

export function CreatePaymentButton() {
return (
<Button
id="create-payment-button"
onClick={async (e) => {
e.preventDefault();

const allowedCredentials = await getAvailableCredentials();

console.log("allowedCredentials", allowedCredentials);

if (!allowedCredentials || allowedCredentials.length === 0) {
return fallbackToIframeCredentialCreation();
}
Expand All @@ -105,29 +115,8 @@ export function CreatePaymentButton() {
challenge: "challenge",
timeout: 60000,
},
{
// - the SPC spec allows for multiple items to be displayed
// - but currently the Chrome implementation does NOT display ANY items
displayItems: [
{
label: "NFT",
amount: { currency: "USD", value: "0.0000001" },
pending: true,
},
{
label: "Gas Fee",
amount: { currency: "USD", value: "0.0000001" },
pending: true,
},
],
total: {
label: "Total",
amount:
// - currency must be a ISO 4217 currency code
{ currency: "USD", value: "0.0000001" },
},
},
"0x",
getPaymentDetails("0.0000001"),
"0x123...456",
);
}}
>
Expand Down
37 changes: 33 additions & 4 deletions apps/dapp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,28 @@ type SerialisableCredentialRequestOptions = Omit<
allowedCredentials: string[];
};

export const getPaymentDetails = (amount: string) => ({
// - the SPC spec allows for multiple items to be displayed
// - but currently the Chrome implementation does NOT display ANY items
displayItems: [
{
label: "NFT",
amount: { currency: "USD", value: "0.0000001" },
pending: true,
},
{
label: "Gas Fee",
amount: { currency: "USD", value: "0.0000001" },
pending: true,
},
],
total: {
label: "Total",
// - currency must be a ISO 4217 currency code
amount: { currency: "USD", value: amount },
},
});

export const payWithSPC = async (
requestOptions: SerialisableCredentialRequestOptions,
paymentDetails: PaymentDetailsInit,
Expand All @@ -39,9 +61,16 @@ export const payWithSPC = async (
const { challenge, timeout } = requestOptions;

// - convert the allowed credential ids to a buffer
const credentialIds = requestOptions.allowedCredentials.map((credentialId) =>
toBuffer(credentialId),
);
const credentialIds = requestOptions.allowedCredentials
.map((credentialId) => {
const credential = toBuffer(credentialId);

// - verify credentialId is reasonable
if (credential.byteLength > 32) return undefined;

return credential;
})
.filter(Boolean);

const paymentMethodData = [
{
Expand All @@ -60,7 +89,7 @@ export const payWithSPC = async (
// A display name and an icon that represent the payment instrument.
instrument: {
...defaultInstrument,
displayName: `Wallet Address - ${address}`,
displayName: `Sent From Wallet - ${address}`,
},

// The origin of the payee
Expand Down
35 changes: 32 additions & 3 deletions apps/dapp/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,41 @@ import WalletIframeDialog from "@/components/wallet-iframe-dialog.astro";
<CreatePaymentButton client:only />
</main>
<script>
import { getPaymentOrigin } from "../lib/utils";
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
import {
getPaymentOrigin,
payWithSPC,
getPaymentDetails,
} from "../lib/utils";
const paymentOrigin = getPaymentOrigin();

window.addEventListener("message", (e) => {
window.addEventListener("message", async (e) => {
if (e.origin !== paymentOrigin) return;
if (e.data.paid !== undefined) return; // TODO: recieve confirmation of payment & display feedback
if (e.data.type === "paid") return; // TODO: recieve confirmation of payment & display feedback
if (e.data.type === "credentials.found") {
console.log("Credentials found", e.data);

// - the wallet has found credentials for the username that was entered into the iframe by the user
// - so we close the iframe and create a payment
const iframeDialog = document.getElementById(
WALLET_IFRAME_DIALOG_ID,
) as HTMLDialogElement | null;

iframeDialog?.close();
iframeDialog?.removeAttribute("open");
iframeDialog?.classList.add("hidden");

// - resubmit the button click event to create the payment
await payWithSPC(
{
allowedCredentials: [e.data.credentialId],
challenge: "challenge",
timeout: 60000,
},
getPaymentDetails("1.00"),
"0x123...456",
);
}
});
</script>
</body>
Expand Down
17 changes: 14 additions & 3 deletions apps/wallet/src/components/forms/username.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Input } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { registerSpcCredential } from "@/lib/utils";
import { registerSpcCredential, sendMessage } from "@/lib/utils";

const formSchema = z.object({
username: z.string().min(2, {
Expand All @@ -26,9 +26,20 @@ export function UsernameForm() {
defaultValues: { username: "" },
});

async function onSubmit(values: z.infer<typeof formSchema>) {
async function onSubmit({ username }: z.infer<typeof formSchema>) {
if (!username) {
const credentialId = await fetch(`/auth/credentials?username=${username}`)
.then((res) => res.text())
.catch(console.error);
if (credentialId) {
localStorage.setItem("credId", credentialId);
sendMessage({ type: "credential.found", payload: { credentialId } });
return;
}
}

await registerSpcCredential({
userId: values.username,
username,
challenge: "challenge",
});
}
Expand Down
16 changes: 10 additions & 6 deletions apps/wallet/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,18 @@ const defaultSpcOpts = {
>;

export const registerSpcCredential = async ({
userId,
username,
challenge,
}: { userId: string; challenge: string }) => {
}: { username: string; challenge: string }) => {
const publicKey = { ...defaultSpcOpts };
const usernameHash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(username),
);

publicKey.user.displayName = userId;
publicKey.user.name = userId;
publicKey.user.id = toBuffer(userId);
publicKey.user.name = username;
publicKey.user.displayName = username;
publicKey.user.id = new Uint8Array(usernameHash);
publicKey.challenge = toBuffer(challenge);

const cred = (await navigator.credentials.create({
Expand Down Expand Up @@ -109,7 +113,7 @@ export const registerSpcCredential = async ({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: userId,
username,
credentialId: cred.id,
publicKey: serialisableCredential.response.publicKey,
dappName: document.referrer,
Expand Down
33 changes: 33 additions & 0 deletions apps/wallet/src/pages/auth/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { APIRoute } from "astro";
import { Credential, db, sql } from "astro:db";

export const GET: APIRoute = async (ctx) => {
try {
const username = ctx.url.searchParams.get("username");
console.log(username);

if (!username) {
return new Response("username is required", { status: 400 });
}

console.log("checking for credential with username", username);

const credentialId =
(
await db
.select({ id: Credential.id })
.from(Credential)
.where(sql`${Credential.username}=${username}`)
.limit(1)
.get()
)?.id ?? "";

return new Response(credentialId, {
status: 200,
});
} catch (e) {
console.log("error", e);

return new Response((e as Error).message, { status: 500 });
}
};
12 changes: 2 additions & 10 deletions apps/wallet/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,13 @@ import { UsernameForm } from "../components/forms/username";
if (e.origin !== getPayeeOrigin()) return;

if (e.data.type === "credentials.get") {
const credential = localStorage.getItem("credId");
let credential = localStorage.getItem("credId");

sendMessage({
type: "credentials.get",
credentials: credential ? [credential] : [],
});
}

if (e.data.paid !== undefined) {
alert(
`Payment ${e.data.paid ? "succeeded" : "failed"}. Credential ${e.data.registered ? "registered" : "not registered"}`,
);
location.href = "/success";
} else {
console.log("recieved message", e.data.message);
}
});
}
</script>
Expand Down

0 comments on commit 3667dee

Please sign in to comment.