Skip to content

Commit

Permalink
Refactor login/signup
Browse files Browse the repository at this point in the history
  • Loading branch information
broody committed May 27, 2024
1 parent b5e1284 commit f7a431f
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 322 deletions.
67 changes: 38 additions & 29 deletions packages/keychain/src/components/Auth/Authenticate.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback, useMemo } from "react";
import React, { useEffect, useState, useCallback, useMemo } from "react";
import { Button } from "@chakra-ui/react";
import { Unsupported } from "./Unsupported";
import { Credentials, onCreateBegin, onCreateFinalize } from "hooks/account";
import { doSignup } from "hooks/account";
import {
FaceIDDuoIcon,
FingerprintDuoIcon,
Expand All @@ -12,36 +12,45 @@ import { PortalBanner } from "components/PortalBanner";
import { PortalFooter } from "components/PortalFooter";
import { requestStorageDropCookie } from "./utils";

type UserAgent = "ios" | "android" | "other";
type AuthAction = "signup" | "login";
``;
export function Authenticate({
name,
onComplete,
action,
onSuccess,
}: {
name: string;
onComplete: () => void;
action: AuthAction;
onSuccess: () => void;
}) {
const [isLoading, setIsLoading] = useState(false);
const [userAgent, setUserAgent] = useState<UserAgent>("other");
const [unsupportedMessage, setUnsupportedMessage] = useState<string>();

const onAuth = useCallback(async () => {
setIsLoading(true);
try {
await requestStorageDropCookie();

const credentials: Credentials = await onCreateBegin(
decodeURIComponent(name),
);
await onCreateFinalize(credentials);
await requestStorageDropCookie();

setTimeout(() => {
onComplete();
}, 2000);
try {
switch (action) {
case "signup":
await doSignup(decodeURIComponent(name));
break;
case "login":
break;
default:
throw new Error(`Unsupported action ${action}`);
}

onSuccess();
} catch (e) {
console.error(e);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.
setIsLoading(false);
throw e;
}
}, [onComplete, name]);
}, [onSuccess, action, name]);

useEffect(() => {
const userAgent = window.navigator.userAgent;
Expand Down Expand Up @@ -73,23 +82,25 @@ export function Authenticate({
return <Unsupported message={unsupportedMessage} />;
}

const title =
action === "signup" ? "Authenticate Yourself" : "Hello from Cartridge!";
const description =
action === "signup" ? (
<>
You will now be asked to authenticate yourself.
<br />
Note: this experience varies from browser to browser.
</>
) : (
<>Please click continue.</>
);
return (
<>
<Container hideAccount>
<PortalBanner
Icon={Icon}
title={isLoading ? "Creating Your Account" : "Authenticate Yourself"}
description={
isLoading ? (
<>This window will close automatically</>
) : (
<>
You will now be asked to authenticate yourself.
<br />
Note: this experience varies from browser to browser.
</>
)
}
Icon={action === "signup" && Icon}
title={title}
description={description}
/>

<PortalFooter>
Expand All @@ -101,5 +112,3 @@ export function Authenticate({
</>
);
}

type UserAgent = "ios" | "android" | "other";
225 changes: 92 additions & 133 deletions packages/keychain/src/components/Auth/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,33 @@
import { Field, Loading } from "@cartridge/ui";
import { Field } from "@cartridge/ui";
import { VStack, Button } from "@chakra-ui/react";
import { Container } from "../Container";
import {
Form as FormikForm,
Field as FormikField,
Formik,
useFormikContext,
} from "formik";
import { Form as FormikForm, Field as FormikField, Formik } from "formik";
import { PortalBanner, PortalFooter } from "components";
import { useCallback, useState } from "react";
import Controller from "utils/controller";
import { FormValues, LoginProps } from "./types";
import { useAnalytics } from "hooks/analytics";
import { beginLogin, onLoginFinalize } from "hooks/account";
import { WebauthnSigner } from "utils/webauthn";
import base64url from "base64url";
import { useClearField } from "./hooks";
import {
requestStorageDropCookie,
fetchAccount,
validateUsernameFor,
} from "./utils";
import { fetchAccount, validateUsernameFor } from "./utils";
import { RegistrationLink } from "./RegistrationLink";
import { useControllerTheme } from "hooks/theme";
import { PopupCenter } from "utils/url";
import { doLogin } from "hooks/account";

export function Login({
prefilledName = "",
chainId,
context,
isSlot,
onController,
onComplete,
onSuccess,
onSignup,
}: LoginProps) {
const [isLoggingIn, setIsLoggingIn] = useState(false);
const { event: log } = useAnalytics();
const theme = useControllerTheme();
const [isLoading, setIsLoading] = useState(false);

const onSubmit = useCallback(
async (values: FormValues) => {
setIsLoggingIn(true);
setIsLoading(true);

const {
account: {
Expand All @@ -49,44 +38,22 @@ export function Login({
},
} = await fetchAccount(values.username);

try {
log({ type: "webauthn_login", address });
console.log("requesting storage drop cookie");
await requestStorageDropCookie();
console.log("requested storage drop cookie");

const { data: beginLoginData } = await beginLogin(values.username);
const signer = new WebauthnSigner(credentialId, publicKey);
const assertion = await signer.sign(
base64url.toBuffer(beginLoginData.beginLogin.publicKey.challenge),
);

const res = await onLoginFinalize(assertion);
if (!res.finalizeLogin) {
throw Error("login failed");
}

const controller = new Controller(address, publicKey, credentialId);

if (onController) {
await onController(controller);
}

if (onComplete) {
onComplete();
}
} catch (err) {
setIsLoggingIn(false);
log({
type: "webauthn_login_error",
payload: {
error: err?.message,
},
address,
});
}
log({ type: "webauthn_login", address });

doLogin(values.username, credentialId, publicKey)
.then(() => onSuccess(new Controller(address, publicKey, credentialId)))
.catch((e) =>
log({
type: "webauthn_login_error",
payload: {
error: e?.message,
},
address,
}),
)
.finally(() => setIsLoading(false));
},
[log, onComplete, onController],
[log, onSuccess],
);

return (
Expand All @@ -97,83 +64,75 @@ export function Login({
validateOnChange={false}
validateOnBlur={false}
>
<Form
context={context}
isSlot={isSlot}
onSignup={onSignup}
isLoggingIn={isLoggingIn}
/>
</Formik>
</Container>
);
}

function Form({
context,
isSlot,
onSignup: onSignupProp,
isLoggingIn,
}: Pick<LoginProps, "context" | "isSlot" | "onSignup"> & {
isLoggingIn: boolean;
}) {
const theme = useControllerTheme();
const { values, isValidating } = useFormikContext<FormValues>();

const onClearUsername = useClearField("username");

const onSignup = useCallback(() => {
onSignupProp(values.username);
}, [onSignupProp, values]);

return (
<FormikForm style={{ width: "100%" }}>
<PortalBanner
title={
theme.id === "cartridge"
? "Play with Cartridge Controller"
: `Play ${theme.name}`
}
description="Enter your Controller username"
/>

<VStack align="stretch">
<FormikField
name="username"
placeholder="Username"
validate={validateUsernameFor("login")}
>
{({ field, meta }) => (
<Field
{...field}
autoFocus
placeholder="Username"
touched={meta.touched}
error={meta.error}
onClear={onClearUsername}
container={{ mb: 6 }}
isLoading={isValidating}
{(props) => (
<FormikForm style={{ width: "100%" }}>
<PortalBanner
title={
theme.id === "cartridge"
? "Play with Cartridge Controller"
: `Play ${theme.name}`
}
description="Enter your Controller username"
/>
)}
</FormikField>
</VStack>

<PortalFooter
origin={context?.origin}
policies={context?.policies}
isSlot={isSlot}
>
<Button
type="submit"
colorScheme="colorful"
isLoading={isLoggingIn}
spinner={<Loading color="solid.primary" />}
>
log in
</Button>
<RegistrationLink description="Need a controller?" onClick={onSignup}>
Sign up
</RegistrationLink>
</PortalFooter>
</FormikForm>
<VStack align="stretch">
<FormikField
name="username"
placeholder="Username"
validate={validateUsernameFor("login")}
>
{({ field, meta }) => (
<Field
{...field}
autoFocus
placeholder="Username"
touched={meta.touched}
error={meta.error}
container={{ mb: 6 }}
isLoading={props.isValidating}
isDisabled={isLoading}
/>
)}
</FormikField>
</VStack>

<PortalFooter
origin={context?.origin}
policies={context?.policies}
isSlot={isSlot}
>
<Button
type="submit"
colorScheme="colorful"
isLoading={isLoading}
onClick={async (ev) => {
document.requestStorageAccess().catch((e) => {
console.error(e);
PopupCenter(
`/authenticate?name=${encodeURIComponent(
props.values.username,
)}&action=login`,
"Cartridge Login",
480,
640,
);
});

ev.preventDefault();
}}
>
Log in
</Button>
<RegistrationLink
description="Need a controller?"
onClick={() => onSignup(props.values.username)}
>
Sign up
</RegistrationLink>
</PortalFooter>
</FormikForm>
)}
</Formik>
</Container>
);
}
Loading

0 comments on commit f7a431f

Please sign in to comment.