Skip to content

Commit

Permalink
Feat: Passkey support (#62)
Browse files Browse the repository at this point in the history
* Feat: Passkey support
  • Loading branch information
drishit96 authored Sep 2, 2024
1 parent 23c6ead commit 4736012
Show file tree
Hide file tree
Showing 27 changed files with 1,499 additions and 181 deletions.
42 changes: 42 additions & 0 deletions app/components/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Ripple } from "@rmwc/ripple";

export default function ListItem({
dataTestId,
hideDivider = false,
index,
expandedIndex,
setExpandedIndex,
content,
expandedContent,
}: {
dataTestId: string;
hideDivider: boolean;
index: number;
expandedIndex?: number;
setExpandedIndex: React.Dispatch<React.SetStateAction<number | undefined>>;
content: React.ReactNode;
expandedContent?: React.ReactNode;
}) {
const [listItemParent] = useAutoAnimate<HTMLDivElement>();
return (
<div ref={listItemParent}>
<Ripple>
<button
data-test-id={dataTestId}
className={`w-full bg-base focus-border border-primary p-2
${hideDivider ? "" : "border-b"}
${expandedIndex === index ? "border-t border-l border-r rounded-t-md" : ""}`}
onClick={(e) => {
e.preventDefault();
setExpandedIndex((prevIndex) => (prevIndex === index ? undefined : index));
}}
>
{content}
</button>
</Ripple>

{expandedIndex === index && expandedContent}
</div>
);
}
170 changes: 170 additions & 0 deletions app/components/Passkey.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import type { PasskeyResponse } from "~/routes/settings/security/passkeys";
import { formatDate_DD_MMMM_YYYY_hh_mm_aa } from "~/utils/date.utils";
import ListItem from "./ListItem";
import type { Navigation, SubmitOptions } from "@remix-run/react";
import { useOutletContext, useSubmit } from "@remix-run/react";
import { Ripple } from "@rmwc/ripple";
import TrashIcon from "./icons/TrashIcon";
import { Spacer } from "./Spacer";
import type { AppContext } from "~/root";
import { Input } from "./Input";
import { useRef, useState } from "react";
import EditIcon from "./icons/EditIcon";

export default function Passkey({
passkey,
navigation,
hideDivider = false,
index,
expandedIndex,
setExpandedIndex,
}: {
passkey: PasskeyResponse;
navigation: Navigation;
hideDivider?: boolean;
index: number;
expandedIndex?: number;
setExpandedIndex: React.Dispatch<React.SetStateAction<number | undefined>>;
}) {
const context = useOutletContext<AppContext>();
const submit = useSubmit();

const passkeyNameRef = useRef(passkey.displayName ?? "");

const isPasskeyNameUpdateInProgress =
navigation.state === "submitting" &&
navigation.formData?.get("formName") === "SAVE_PASSKEY_NAME" &&
navigation.formData?.get("passkeyId") === passkey.id;

const isPasskeyDeletionInProgress =
navigation.state === "submitting" &&
navigation.formMethod === "DELETE" &&
navigation.formData?.get("passkeyId") === passkey.id;

return (
<ListItem
dataTestId={`passkey-${passkey.displayName}`}
hideDivider={hideDivider}
index={index}
expandedIndex={expandedIndex}
setExpandedIndex={setExpandedIndex}
content={
<div className="flex flex-col items-start">
<p className="text-lg text-primary font-bold">
{passkey.displayName ?? "Passkey"}
</p>
<p className="text-sm text-secondary">
Created on:{" "}
{passkey.createdAt
? formatDate_DD_MMMM_YYYY_hh_mm_aa(new Date(passkey.createdAt))
: "Never"}
</p>
<p className="text-sm text-secondary">
Last used:{" "}
{passkey.lastUsed
? formatDate_DD_MMMM_YYYY_hh_mm_aa(new Date(passkey.lastUsed))
: "Never"}
</p>
</div>
}
expandedContent={
<div>
<div className="w-full flex border-b border-t border-primary rounded-b-md bg-base">
<div className="flex-1 cursor-pointer border-l border-primary">
<Ripple>
<button
data-test-id={"btn-delete"}
className="flex flex-col w-full p-3 items-center focus-border"
type="submit"
disabled={isPasskeyNameUpdateInProgress}
onClick={(e) => {
e.preventDefault();
context.setDialogProps({
title: "Edit passkey name",
message: <PasskeyNameInput passkeyNameRef={passkeyNameRef} />,
showDialog: true,
positiveButton: "Save",
onPositiveClick: () => {
const form = new FormData();
form.set("formName", "SAVE_PASSKEY_NAME");
form.set("passkeyId", passkey.id);
form.set("passkeyName", passkeyNameRef.current);
const submitOptions: SubmitOptions = {
method: "POST",
replace: true,
};
submit(form, submitOptions);
},
});
}}
>
<EditIcon size={24} />
<Spacer size={1} />
{isPasskeyNameUpdateInProgress ? "Saving..." : "Edit name"}
</button>
</Ripple>
</div>
<div className="flex-1 cursor-pointer border-l border-r border-primary">
<Ripple>
<button
data-test-id={"btn-delete"}
className="flex flex-col w-full p-3 items-center focus-border"
type="submit"
disabled={isPasskeyDeletionInProgress}
onClick={(e) => {
e.preventDefault();
context.setDialogProps({
title: "Delete passkey?",
message:
"By removing this passkey you will no longer be able to use it to sign-in to your account from any of the devices on which it has been synced. Continue with deletion?",
showDialog: true,
positiveButton: "Delete",
onPositiveClick: () => {
const form = new FormData();
form.set("formName", "DELETE_PASSKEY");
form.set("passkeyId", passkey.id);
const submitOptions: SubmitOptions = {
method: "DELETE",
replace: true,
};
submit(form, submitOptions);
},
});
}}
>
<TrashIcon size={24} />
<Spacer size={1} />
{isPasskeyDeletionInProgress ? "Deleting..." : "Delete"}
</button>
</Ripple>
</div>
</div>
</div>
}
></ListItem>
);
}

export function PasskeyNameInput({
passkeyNameRef,
}: {
passkeyNameRef: React.MutableRefObject<string>;
}) {
const [passkeyName, setPasskeyName] = useState(passkeyNameRef.current ?? "");
return (
<>
<Input
name="passkeyName"
label="Passkey name"
autoFocus
required
type="text"
value={passkeyName}
onChangeHandler={(e) => {
passkeyNameRef.current = e.target.value;
setPasskeyName(e.target.value);
}}
/>
</>
);
}
84 changes: 38 additions & 46 deletions app/components/Transaction.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { Navigation } from "@remix-run/router";
import { Ripple } from "@rmwc/ripple";
import type { SubmitOptions } from "@remix-run/react";
Expand All @@ -13,6 +12,7 @@ import InfoIcon from "./icons/InfoIcon";
import RepeatIcon from "./icons/RepeatIcon";
import { Spacer } from "./Spacer";
import TrashIcon from "./icons/TrashIcon";
import ListItem from "./ListItem";

export function Transaction({
transaction,
Expand All @@ -31,7 +31,6 @@ export function Transaction({
setExpandedIndex: React.Dispatch<React.SetStateAction<number | undefined>>;
submitAction?: string;
}) {
const [listItemParent] = useAutoAnimate<HTMLDivElement>();
const context = useOutletContext<AppContext>();
const submit = useSubmit();
const isTransactionUpdateInProgress =
Expand All @@ -40,49 +39,42 @@ export function Transaction({
navigation.formData?.get("transactionId") === transaction.id;

return (
<div ref={listItemParent}>
<Ripple>
<button
data-test-id={`more-${transaction.category.split(" ").join("")}-${
transaction.amount
}`}
className={`w-full bg-base focus-border border-primary p-2
${hideDivider ? "" : "border-b"}
${expandedIndex === index ? "border-t border-l border-r rounded-t-md" : ""}`}
onClick={(e) => {
e.preventDefault();
setExpandedIndex((prevIndex) => (prevIndex === index ? undefined : index));
}}
>
<div className="flex flex-col">
<div className="flex">
<span className="font-bold">{transaction.category}</span>
<span className="flex-grow"></span>
<span
className={
getTransactionColor(transaction.type) + " font-bold tabular-nums"
}
>
{transaction.type === "income" ? "+" : "-"}
{formatNumber(
transaction.amount.toString(),
context.userPreferredLocale ?? context.locale
)}
</span>
</div>
<Spacer size={1} />
<div className="flex">
<span className="text-gray-500">
{formatDate_DD_MMMM_YYYY(new Date(transaction.createdAt))}
</span>
<span className="flex-grow"></span>
<span className="text-gray-500">{transaction.paymentMode}</span>
</div>
<ListItem
dataTestId={`more-${transaction.category.split(" ").join("")}-${
transaction.amount
}`}
hideDivider={hideDivider}
index={index}
expandedIndex={expandedIndex}
setExpandedIndex={setExpandedIndex}
content={
<div className="flex flex-col">
<div className="flex">
<span className="font-bold">{transaction.category}</span>
<span className="flex-grow"></span>
<span
className={
getTransactionColor(transaction.type) + " font-bold tabular-nums"
}
>
{transaction.type === "income" ? "+" : "-"}
{formatNumber(
transaction.amount.toString(),
context.userPreferredLocale ?? context.locale
)}
</span>
</div>
</button>
</Ripple>

{expandedIndex === index && (
<Spacer size={1} />
<div className="flex">
<span className="text-gray-500">
{formatDate_DD_MMMM_YYYY(new Date(transaction.createdAt))}
</span>
<span className="flex-grow"></span>
<span className="text-gray-500">{transaction.paymentMode}</span>
</div>
</div>
}
expandedContent={
<div>
{transaction.description && (
<div className="text-primary border-l border-r border-primary bg-base">
Expand Down Expand Up @@ -166,7 +158,7 @@ export function Transaction({
</Form>
</div>
</div>
)}
</div>
}
></ListItem>
);
}
11 changes: 9 additions & 2 deletions app/components/Turnstile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { useEffect } from "react";
import { TURNSTILE_SITE_KEY } from "~/lib/ui.config";
import type { ChallengeAction } from "~/modules/user/user.service";

export default function Turnstile({ action }: { action: ChallengeAction }) {
export default function Turnstile({
action,
onNewToken,
}: {
action: ChallengeAction;
onNewToken: (token: string) => void;
}) {
useEffect(() => {
if (window.turnstile) {
window.turnstile.remove(window.turnstileWidgetId);
Expand All @@ -11,13 +17,14 @@ export default function Turnstile({ action }: { action: ChallengeAction }) {
const script = document.createElement("script");
script.async = true;
script.src =
"https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback";
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback";
document.body.appendChild(script);

window.onloadTurnstileCallback = function () {
window.turnstileWidgetId = window.turnstile.render("#cf-turnstile", {
sitekey: TURNSTILE_SITE_KEY,
action,
callback: onNewToken,
});
};
}, []);
Expand Down
1 change: 1 addition & 0 deletions app/modules/settings/security/mfa.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export async function verify2FAToken(userId: string, token: string) {
return verifyAuthenticatorToken(token, decoder.decode(decryptedSecret));
} catch (error) {
logError(error);
return false;
}
}

Expand Down
Loading

0 comments on commit 4736012

Please sign in to comment.