Skip to content

Commit

Permalink
Fix Signup popup in Safari by removing async logic on cta (#533)
Browse files Browse the repository at this point in the history
* Fix Signup popup in Safari by removing async logic on cta

* Remove react-hook-form deps from login

* Add workaround note for signup validation logic

* Fix error message on signup/login form
  • Loading branch information
JunichiSugiura authored Aug 6, 2024
1 parent 62cea53 commit 20b1e71
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 154 deletions.
1 change: 0 additions & 1 deletion packages/keychain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"next": "^13.4.19",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.1",
"react-query": "^3.39.2",
"starknet": "^6.11.0",
"use-sound": "^4.0.1",
Expand Down
178 changes: 87 additions & 91 deletions packages/keychain/src/components/connect/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Field } from "@cartridge/ui";
import { Button } from "@chakra-ui/react";
import { Container, Footer, Content, useLayout } from "components/layout";
import { SubmitHandler, useForm, useController } from "react-hook-form";
import { useCallback, useEffect, useState } from "react";
import Controller from "utils/controller";
import { FormInput, LoginMode, LoginProps } from "./types";
import { LoginMode, LoginProps } from "./types";
import { useAnalytics } from "hooks/analytics";
import { fetchAccount, validateUsernameFor } from "./utils";
import { RegistrationLink } from "./RegistrationLink";
Expand Down Expand Up @@ -44,102 +43,97 @@ function Form({
const [isLoading, setIsLoading] = useState(false);
const [expiresAt] = useState<bigint>(3000000000n);
const [error, setError] = useState<Error>();

const { handleSubmit, formState, control, setValue } = useForm<FormInput>({
defaultValues: { username: prefilledName },
});
const { field: usernameField } = useController({
name: "username",
control,
rules: {
required: "Username required",
minLength: {
value: 3,
message: "Username must be at least 3 characters",
},
validate: validateUsernameFor("login"),
},
const [usernameField, setUsernameField] = useState({
value: prefilledName,
error: undefined,
});
const [isValidating, setIsValidating] = useState(false);

const onSubmit: SubmitHandler<FormInput> = useCallback(
async (values) => {
setIsLoading(true);
const onSubmit = useCallback(async () => {
setIsValidating(true);
const error = await validateUsernameFor("login")(usernameField.value);
if (error) {
setUsernameField((u) => ({ ...u, error }));
}

const {
account: {
credentials: {
webauthn: [{ id: credentialId, publicKey }],
},
contractAddress: address,
setIsValidating(false);

setIsLoading(true);

const {
account: {
credentials: {
webauthn: [{ id: credentialId, publicKey }],
},
} = await fetchAccount(values.username);

try {
const controller = new Controller({
chainId,
rpcUrl,
address,
username: values.username,
publicKey,
credentialId,
});

switch (mode) {
case LoginMode.Webauthn:
await doLogin(values.username, credentialId);
break;
case LoginMode.Controller:
if (policies.length === 0) {
throw new Error("Policies required for controller ");
}

await controller.approve(origin, expiresAt, policies);
break;
}

controller.store();
setController(controller);

if (onSuccess) {
onSuccess();
}

log({ type: "webauthn_login", address });
} catch (e) {
setError(e);

log({
type: "webauthn_login_error",
payload: {
error: e?.message,
},
address,
});
contractAddress: address,
},
} = await fetchAccount(usernameField.value);

try {
const controller = new Controller({
chainId,
rpcUrl,
address,
username: usernameField.value,
publicKey,
credentialId,
});

switch (mode) {
case LoginMode.Webauthn:
await doLogin(usernameField.value, credentialId);
break;
case LoginMode.Controller:
if (policies.length === 0) {
throw new Error("Policies required for controller ");
}

await controller.approve(origin, expiresAt, policies);
break;
}

setIsLoading(false);
},
[
chainId,
rpcUrl,
origin,
policies,
expiresAt,
mode,
log,
onSuccess,
setController,
],
);
controller.store();
setController(controller);

if (onSuccess) {
onSuccess();
}

log({ type: "webauthn_login", address });
} catch (e) {
setError(e);

log({
type: "webauthn_login_error",
payload: {
error: e?.message,
},
address,
});
}

setIsLoading(false);
}, [
usernameField.value,
chainId,
rpcUrl,
origin,
policies,
expiresAt,
mode,
log,
onSuccess,
setController,
]);

useEffect(() => {
if (!formState.isValidating || !footer.isOpen) return;
if (!isValidating || !footer.isOpen) return;
footer.onToggle();
}, [formState.isValidating, footer]);
}, [isValidating, footer]);
return (
<form
style={{ width: "100%" }}
onSubmit={handleSubmit(onSubmit)}
onSubmit={onSubmit}
onChange={() => setError(undefined)}
>
<Content>
Expand All @@ -148,16 +142,18 @@ function Form({
autoFocus
onChange={(e) => {
setError(undefined);
e.target.value = e.target.value.toLowerCase();
usernameField.onChange(e);
setUsernameField((u) => ({
...u,
value: e.target.value.toLowerCase(),
}));
}}
placeholder="Username"
error={formState.errors.username}
isLoading={formState.isValidating}
error={usernameField.error}
isLoading={isValidating}
isDisabled={isLoading}
onClear={() => {
setError(undefined);
setValue(usernameField.name, "");
setUsernameField((u) => ({ ...u, value: "" }));
}}
/>
</Content>
Expand Down
78 changes: 42 additions & 36 deletions packages/keychain/src/components/connect/Signup.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Field } from "@cartridge/ui";
import { Button } from "@chakra-ui/react";
import { Container, Footer, Content } from "components/layout";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useAccountQuery } from "generated/graphql";
import Controller from "utils/controller";
import { PopupCenter } from "utils/url";
import { FormInput, SignupProps } from "./types";
import { SignupProps } from "./types";
import { isIframe, validateUsernameFor } from "./utils";
import { RegistrationLink } from "./RegistrationLink";
import { doSignup } from "hooks/account";
import { useControllerTheme } from "hooks/theme";
import { useConnection } from "hooks/connection";
import { ErrorAlert } from "components/ErrorAlert";
import { useDeploy } from "hooks/deploy";
import { useController, useForm } from "react-hook-form";
import { constants } from "starknet";
import { useDebounce } from "hooks/debounce";

export function Signup({
prefilledName = "",
Expand All @@ -27,27 +27,36 @@ export function Signup({
const { deployRequest } = useDeploy();
const [error, setError] = useState<Error>();
const [isRegistering, setIsRegistering] = useState(false);
const [usernameField, setUsernameField] = useState({
value: prefilledName,
error: undefined,
});
const [isValidating, setIsValidating] = useState(false);

const {
handleSubmit,
formState,
control,
setValue,
setError: setFieldError,
} = useForm<FormInput>({ defaultValues: { username: prefilledName } });
const { hasPrefundRequest } = useConnection();
const { field: usernameField } = useController({
name: "username",
control,
rules: {
required: "Username required",
minLength: {
value: 3,
message: "Username must be at least 3 characters",
},
validate: validateUsernameFor("signup"),
},
});
const { debouncedValue: username, debouncing } = useDebounce(
usernameField.value,
1000,
);

// In order for Safari to open "Create Passkey" modal successfully, submit handler has to be async.
// The workaround is to call async validation function every time when username input changes
useEffect(() => {
setError(undefined);

if (username) {
const validate = async () => {
setIsValidating(true);
const error = await validateUsernameFor("signup")(username);
if (error) {
setUsernameField((u) => ({ ...u, error }));
}

setIsValidating(false);
};
validate();
}
}, [username]);

const onSubmit = useCallback(() => {
setError(undefined);
Expand All @@ -74,13 +83,10 @@ export function Signup({

doSignup(decodeURIComponent(usernameField.value))
.catch((e) => {
setFieldError(usernameField.name, {
type: "custom",
message: e.message,
});
setUsernameField((u) => ({ ...u, error: e.message }));
})
.finally(() => setIsRegistering(false));
}, [setFieldError, usernameField]);
}, [usernameField]);

// for polling approach when iframe
useAccountQuery(
Expand Down Expand Up @@ -154,16 +160,18 @@ export function Signup({
autoFocus
onChange={(e) => {
setError(undefined);
e.target.value = e.target.value.toLowerCase();
usernameField.onChange(e);
setUsernameField((u) => ({
...u,
value: e.target.value.toLowerCase(),
}));
}}
placeholder="Username"
error={formState.errors.username}
isLoading={formState.isValidating}
error={usernameField.error}
isLoading={isValidating}
isDisabled={isRegistering}
onClear={() => {
setError(undefined);
setValue(usernameField.name, "");
setUsernameField((u) => ({ ...u, value: "" }));
}}
/>
</Content>
Expand All @@ -176,10 +184,8 @@ export function Signup({
<Button
colorScheme="colorful"
isLoading={isRegistering}
isDisabled={
!!Object.keys(formState.errors).length || formState.isValidating
}
onClick={handleSubmit(onSubmit)}
isDisabled={debouncing || !username || isValidating}
onClick={onSubmit}
>
sign up
</Button>
Expand Down
6 changes: 5 additions & 1 deletion packages/keychain/src/components/connect/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { AuthAction } from "./Authenticate";

export function validateUsernameFor(type: AuthAction) {
return async (val: string) => {
if (val.split(" ").length > 1) {
if (!val) {
return "Username required";
} else if (val.length < 3) {
return "Username must be at least 3 characters";
} else if (val.split(" ").length > 1) {
return "Username cannot contain spaces";
}

Expand Down
3 changes: 1 addition & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"vite-plugin-dts": "^3.5.3"
},
"dependencies": {
"@chakra-ui/anatomy": "^2.2.1",
"react-hook-form": "^7.52.1"
"@chakra-ui/anatomy": "^2.2.1"
}
}
Loading

0 comments on commit 20b1e71

Please sign in to comment.