Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add new react hooks to improve devex #5

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@

import { LoginSignupCard } from "@/components/LoginSignupCard";
import { UserCard } from "@/components/UserCard";
import { AccountContextProvider } from "@/context/AccountContext";
import { useSignerContext } from "@/context/SignerContext";
import { useAccount, useSignerStatus } from "@alchemy/aa-alchemy/react";

export default function Home() {
const { signer, account, isLoadingUser, refetchUserDetails } =
useSignerContext();
const signerStatus = useSignerStatus();
const { account } = useAccount({
type: "MultiOwnerModularAccount",
});

return (
<main className="flex min-h-screen flex-col items-center p-24 gap-4 justify-center">
{isLoadingUser ? (
{signerStatus.isInitializing ? (
<span className="loading loading-ring loading-lg"></span>
) : account == null ? (
<LoginSignupCard signer={signer} onLogin={refetchUserDetails} />
<LoginSignupCard />
) : (
<AccountContextProvider account={account}>
<UserCard />
</AccountContextProvider>
<UserCard />
)}
</main>
);
Expand Down
32 changes: 15 additions & 17 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
"use client";
import { SignerContextProvider } from "@/context/SignerContext";
import { createConfig } from "@alchemy/aa-alchemy/config";
import { AlchemyAccountProvider } from "@alchemy/aa-alchemy/react";
import { sepolia } from "@alchemy/aa-core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PropsWithChildren, Suspense, useState } from "react";
import { PropsWithChildren, Suspense } from "react";

const TurnkeyIframeContainerId = "turnkey-iframe-container-id";
const TurnkeyIframeElementId = "turnkey-iframe-element-id";
const config = createConfig({
// required
rpcUrl: "/api/rpc",
chain: sepolia,
// optional
rootOrgId: "3121a8a0-c548-4d14-a313-630c3b739858",
});

export const Providers = (props: PropsWithChildren) => {
const [queryClient] = useState(() => new QueryClient());
const [clientConfig] = useState({
connection: {
rpcUrl: "/api/rpc",
},
iframeConfig: {
iframeContainerId: TurnkeyIframeContainerId,
iframeElementId: TurnkeyIframeElementId,
},
});
const queryClient = new QueryClient();

export const Providers = (props: PropsWithChildren<{}>) => {
return (
<Suspense>
<QueryClientProvider client={queryClient}>
<SignerContextProvider client={clientConfig}>
<AlchemyAccountProvider config={config} queryClient={queryClient}>
{props.children}
</SignerContextProvider>
</AlchemyAccountProvider>
</QueryClientProvider>
</Suspense>
);
Expand Down
7 changes: 0 additions & 7 deletions client.ts

This file was deleted.

22 changes: 0 additions & 22 deletions components/EmailBundleForm.tsx

This file was deleted.

42 changes: 17 additions & 25 deletions components/LoginSignupCard.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,56 @@
"use client";
import { AlchemySigner } from "@alchemy/aa-alchemy";
import { useMutation } from "@tanstack/react-query";
import { useAccount, useAuthenticate } from "@alchemy/aa-alchemy/react";
import { useState } from "react";
import EmailBundleForm from "./EmailBundleForm";
import EmailForm from "./EmailForm";

type Props = {
signer?: AlchemySigner;
onLogin: () => void;
};

export const LoginSignupCard = ({ signer, onLogin }: Props) => {
export const LoginSignupCard = () => {
const [email, setEmail] = useState<string | undefined>(undefined);

const { mutate, isPending } = useMutation({
mutationFn: signer?.authenticate,
onSuccess: onLogin,
onError: (e) => {
console.error("Failed to login", e);
},
const { authenticate, isPending } = useAuthenticate();
const { isLoadingAccount } = useAccount({
type: "MultiOwnerModularAccount",
skipCreate: true,
});

return (
<div className="card bg-base-100 shadow-xl w-[500px] max-w-[500px]">
<div className="card-body gap-4">
<h2 className="card-title">Login / Signup</h2>
{email && isPending ? (
// OTP bundle input
<EmailBundleForm
onSubmit={(bundle) => {
// resolve(bundle);
}}
/>
<div>Check your email and click the link to complete login</div>
) : (
// email input
<>
<EmailForm
buttonDisabled={isLoadingAccount || isPending}
onSubmit={(email) => {
setEmail(email);
mutate({
authenticate({
type: "email",
email,
});
}}
/>
<div className="flex flex-row gap-2">
<button
className="btn btn-ghost btn-sm"
onClick={() =>
mutate({
authenticate({
type: "passkey",
createNew: true,
username: "Test User",
})
}
disabled={isLoadingAccount || isPending}
>
Use New Passkey
</button>
<div className="divider divider-horizontal"></div>
<button
onClick={() => mutate({ type: "passkey", createNew: false })}
className="btn btn-ghost btn-sm"
onClick={() =>
authenticate({ type: "passkey", createNew: false })
}
disabled={isLoadingAccount || isPending}
>
Use Existing Passkey
</button>
Expand Down
134 changes: 59 additions & 75 deletions components/UserCard.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,60 @@
"use client";
import { publicClient } from "@/client";
import { useAccountContext } from "@/context/AccountContext";
import { useSignerContext } from "@/context/SignerContext";
import {
useAddPasskey,
useBundlerClient,
useExportAccount,
useLogout,
useSignMessage,
useSmartAccountClient,
useUser,
} from "@alchemy/aa-alchemy/react";
import { useForm } from "@tanstack/react-form";
import { useMutation } from "@tanstack/react-query";
import { zodValidator } from "@tanstack/zod-form-adapter";
import { useCallback, useState } from "react";
import { z } from "zod";

const TurnkeyExportWalletContainerId = "turnkey-export-wallet-container-id";
const TurnkeyExportWalletElementId = "turnkey-export-wallet-element-id";

const iframeCss = `
iframe {
box-sizing: border-box;
width: 100%;
height: 120px;
border-radius: 8px;
border-width: 1px;
border-style: solid;
border-color: rgba(216, 219, 227, 1);
padding: 20px;
}
`;

export const UserCard = () => {
const { signer, user, account } = useSignerContext();
const { provider } = useAccountContext();
const bundlerClient = useBundlerClient();
const { client, isLoadingClient } = useSmartAccountClient({
type: "MultiOwnerModularAccount",
});
const user = useUser();

const { mutate: signMessage, data: { signature, isValid } = {} } =
useMutation({
mutationFn: async (msg: string) => {
return provider
.signMessageWith6492({ message: msg })
.then(async (signature) => {
return {
signature,
isValid: await publicClient
.verifyMessage({
address: provider.getAddress(),
message: msg,
signature,
})
.catch((e: any) => {
console.log("error verifying signature, ", e);
return false;
}),
};
});
},
});
const { signMessageAsync, signedMessage } = useSignMessage({ client });
const [isValid, setIsValid] = useState<boolean>(false);
const signMessageAndVerify = useCallback(
async ({ message }: { message: string }) => {
if (!client) {
return;
}

const { mutate, isPending, data } = useMutation({
mutationFn: async () =>
signer.exportWallet({
iframeContainerId: TurnkeyExportWalletContainerId,
iframeElementId: TurnkeyExportWalletElementId,
}),
});
const signature = await signMessageAsync({ message });
const isValid = await bundlerClient.verifyMessage({
message,
signature,
address: client.getAddress(),
});

const { mutate: addPasskey } = useMutation({
mutationFn: async () => signer.addPasskey({}),
onSuccess: (data) => {
console.log(data);
},
});
setIsValid(isValid);

const { mutate: logout } = useMutation({
mutationFn: async () => signer.disconnect(),
onSuccess: () => {
window.location.reload();
return { signature, isValid };
},
});
[bundlerClient, client, signMessageAsync]
);

const { exportAccount, isExporting, isExported, ExportAccountComponent } =
useExportAccount();

const { addPasskey } = useAddPasskey();

const { logout } = useLogout({ onSuccess: window.location.reload });

const form = useForm({
defaultValues: {
message: "",
},
validatorAdapter: zodValidator,
onSubmit: ({ value }) => signMessage(value.message),
onSubmit: ({ value }) => signMessageAndVerify({ message: value.message }),
});

return (
Expand All @@ -98,7 +75,7 @@ export const UserCard = () => {
</div>
<div className="flex flex-col">
<strong>Account Address</strong>
<code className="break-words">{account!.address}</code>
<code className="break-words">{client?.account.address}</code>
</div>
<div className="flex flex-col">
<strong>Signer Address</strong>
Expand Down Expand Up @@ -144,7 +121,7 @@ export const UserCard = () => {
{({ canSubmit, isSubmitting }) => (
<button
className="btn"
disabled={!canSubmit || isSubmitting}
disabled={!canSubmit || isSubmitting || isLoadingClient}
type="submit"
>
Submit
Expand All @@ -153,11 +130,11 @@ export const UserCard = () => {
</form.Subscribe>
</form>
</form.Provider>
{signature && (
{signedMessage && (
<>
<div className="flex flex-col">
<strong>Signature</strong>
<code className="break-words">{signature}</code>
<code className="break-words">{signedMessage}</code>
</div>
<div className="flex flex-col">
<strong>Is Valid?</strong>
Expand All @@ -166,20 +143,27 @@ export const UserCard = () => {
</>
)}
<div className="flex flex-col gap-2">
{!data ? (
<button onClick={() => mutate()} disabled={isPending}>
{!isExported ? (
<button onClick={() => exportAccount()} disabled={isExporting}>
Export Wallet
</button>
) : (
<strong>Seed Phrase</strong>
)}
<div
<ExportAccountComponent
className="w-full"
style={{ display: !data ? "none" : "block" }}
id={TurnkeyExportWalletContainerId}
>
<style>{iframeCss}</style>
</div>
iframeCss={{
boxSizing: "border-box",
width: "100%",
height: "120px",
borderRadius: "8px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "rgba(216, 219, 227, 1)",
padding: "20px",
}}
isExported={isExported}
/>
</div>
</div>
</div>
Expand Down
Loading