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

Generalize remote session creation #608

Merged
merged 18 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions packages/keychain/src/components/connect/CreateController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { isSignedUp } from "../../utils/cookie";
export function CreateController({
isSlot,
loginMode,
onCreated,
}: {
isSlot?: boolean;
loginMode?: LoginMode;
onCreated?: () => void;
}) {
const { error } = useConnection();
const [showSignup, setShowSignup] = useState(true);
Expand All @@ -32,6 +34,7 @@ export function CreateController({
onLogin={(username) => {
setPrefilledUsername(username);
setShowSignup(false);
onCreated?.();
}}
isSlot={isSlot}
/>
Expand All @@ -41,6 +44,7 @@ export function CreateController({
onSignup={(username) => {
setPrefilledUsername(username);
setShowSignup(true);
onCreated?.();
}}
mode={loginMode}
isSlot={isSlot}
Expand Down
39 changes: 39 additions & 0 deletions packages/keychain/src/pages/failure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { Container } from "components/layout";
import { AlertIcon, ExternalIcon } from "@cartridge/ui";
import { Link, Text } from "@chakra-ui/react";
import NextLink from "next/link";
import { CARTRIDGE_DISCORD_LINK } from "const";

export default function Failure() {
return (
<Container
variant="connect"
hideAccount
icon={<AlertIcon boxSize={9} />}
title="Uh-oh something went wrong"
description={
<>
If this problem persists swing by the Cartridge
<Text color="inherit">
support channel on{" "}
<Link
as={NextLink}
href={CARTRIDGE_DISCORD_LINK}
isExternal
color="link.blue"
display="inline-flex"
flexDir="row"
columnGap="0.1rem"
alignItems="center"
>
Discord
<ExternalIcon />
</Link>
</Text>
</>
}
/>
);
}
23 changes: 15 additions & 8 deletions packages/keychain/src/pages/login.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
"use client";

import { useRouter } from "next/router";
import { Login as LoginComponent } from "components/connect";
import { useConnection } from "hooks/connection";
import dynamic from "next/dynamic";

function Login() {
export default function Login() {
const router = useRouter();
const { controller, rpcUrl, chainId, error } = useConnection();

const navigateToSuccess = () => {
router.replace({
pathname: "/success",
query: {
title: "Logged in!",
description: "Your controller is ready",
},
});
};

if (error) {
return <>{error.message}</>;
}

if (controller) {
router.replace(`${process.env.NEXT_PUBLIC_ADMIN_URL}/profile`);
navigateToSuccess();
}

if (!rpcUrl || !chainId) {
Expand All @@ -22,11 +33,7 @@ function Login() {
return (
<LoginComponent
onSignup={() => router.push({ pathname: "/signup", query: router.query })}
onSuccess={async () => {
router.replace(`${process.env.NEXT_PUBLIC_ADMIN_URL}/profile`);
}}
onSuccess={navigateToSuccess}
/>
);
}

export default dynamic(() => Promise.resolve(Login), { ssr: false });
123 changes: 123 additions & 0 deletions packages/keychain/src/pages/session.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use client";

import { Policy } from "@cartridge/controller";
import { CreateController, CreateSession as CreateSessionComp } from "components/connect";

import { useConnection } from "hooks/connection";
import { useRouter } from "next/router";
import { useCallback, useEffect } from "react";
import { Call, hash } from "starknet";
import { LoginMode } from "components/connect/types";

type SessionQueryParams = Record<string, string> & {
callback_uri: string;
};

/**
This page is for creating session
*/
export default function CreateRemoteSession() {
const router = useRouter();
const queries = router.query as SessionQueryParams;

const { controller, policies, origin } =
useConnection();

const navigateBackHere = useCallback(() => {
router.replace({
pathname: "/session",
query: router.query,
});
}, [router]);

// Handler for calling the callback uri.
// Send the session details to the callback uri in the body of the
// POST request. If the request is successful, then redirect to the
// success page. Else, redirect to the failure page.
const onCallback = useCallback(() => {
const url = new URL(decodeURIComponent(queries.callback_uri));
const session = controller.account.sessionJson();
if (!url || !session) {
router.replace(`/failure`);
return;
}

const headers = new Headers();
headers.append("Content-Type", "application/json");

fetch(url, {
body: JSON.stringify({
username: controller.username,
credentials: {
publicKey: controller.publicKey,
credentialId: controller.credentialId,
},
session,
}),
headers,
method: "POST",
})
Fixed Show fixed Hide fixed
.then(async (res) => {
if (res.ok) {
return router.replace({
pathname: "/success",
query: {
title: "Seession Created!",
description: "Return to your terminal to continue",
},
});
}

Promise.reject();
})
.catch((e) => {
console.error("failed to call the callback url", e);
router.replace(`/failure`);
});
}, [router, queries.callback_uri, controller]);

// Handler when user clicks the Create button
const onConnect = useCallback(
(_: Policy[]) => {
if (!controller.account.sessionJson()) {
throw new Error("Session not found");
}

if (!queries.callback_uri) {
throw new Error("Callback URI is missing");
}

onCallback();
},
[queries.callback_uri, controller, onCallback],
);

// Once we have a connected controller initialized, check if a session already exists.
// If yes, check if the policies of the session are the same as the ones that are
// currently being requested. Return existing session to the callback uri if policies match.
useEffect(() => {
if (!controller || !origin) {
return;
}

let calls = policies.map((policy) => {
return {
contractAddress: policy.target,
entrypoint: hash.getSelector(policy.method),
calldata: [],
} as Call;
});

// if the requested policies has no mismatch with existing policies then return
// the exising session
if (controller.account.hasSession(calls)) {
onCallback();
}
}, [controller, origin, policies, onCallback]);

return controller ? (
<CreateController loginMode={LoginMode.Controller} onCreated={navigateBackHere} />
) : (
<CreateSessionComp onConnect={onConnect} />
);
}
23 changes: 15 additions & 8 deletions packages/keychain/src/pages/signup.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
"use client";

import { useRouter } from "next/router";
import { Signup as SignupComponent } from "components/connect";
import { useConnection } from "hooks/connection";
import dynamic from "next/dynamic";

function Signup() {
export default function Signup() {
const router = useRouter();
const { controller, rpcUrl, chainId, error } = useConnection();

const navigateToSuccess = (title: string) => {
router.replace({
pathname: "/success",
query: {
title,
description: "Your controller is ready",
},
});
};

if (error) {
return <>{error.message}</>;
}

if (controller) {
router.replace(`${process.env.NEXT_PUBLIC_ADMIN_URL}/profile`);
navigateToSuccess("Already signed up!");
}

if (!rpcUrl || !chainId) {
Expand All @@ -22,11 +33,7 @@ function Signup() {
return (
<SignupComponent
onLogin={() => router.push({ pathname: "/login", query: router.query })}
onSuccess={() => {
router.replace(`${process.env.NEXT_PUBLIC_ADMIN_URL}/profile`);
}}
onSuccess={() => navigateToSuccess("Signed up!")}
/>
);
}

export default dynamic(() => Promise.resolve(Signup), { ssr: false });
Loading
Loading