diff --git a/packages/keychain/next.config.js b/packages/keychain/next.config.js
index 499141eab..ff54c4a6b 100644
--- a/packages/keychain/next.config.js
+++ b/packages/keychain/next.config.js
@@ -20,6 +20,25 @@ const nextConfig = {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
+ redirects: async function () {
+ return [
+ {
+ source: '/slot/auth',
+ destination: '/slot',
+ permanent: true
+ },
+ {
+ source: '/slot/auth/success',
+ destination: '/success',
+ permanent: true
+ },
+ {
+ source: '/slot/auth/failure',
+ destination: '/failure',
+ permanent: true
+ }
+ ]
+ }
};
module.exports = nextConfig;
diff --git a/packages/keychain/src/components/connect/CreateController.tsx b/packages/keychain/src/components/connect/CreateController.tsx
index 8d5c82e2d..81b00398e 100644
--- a/packages/keychain/src/components/connect/CreateController.tsx
+++ b/packages/keychain/src/components/connect/CreateController.tsx
@@ -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);
@@ -32,6 +34,7 @@ export function CreateController({
onLogin={(username) => {
setPrefilledUsername(username);
setShowSignup(false);
+ onCreated?.();
}}
isSlot={isSlot}
/>
@@ -41,6 +44,7 @@ export function CreateController({
onSignup={(username) => {
setPrefilledUsername(username);
setShowSignup(true);
+ onCreated?.();
}}
mode={loginMode}
isSlot={isSlot}
diff --git a/packages/keychain/src/pages/slot/auth/failure.tsx b/packages/keychain/src/pages/failure.tsx
similarity index 88%
rename from packages/keychain/src/pages/slot/auth/failure.tsx
rename to packages/keychain/src/pages/failure.tsx
index 9d9a84b0d..2ba986660 100644
--- a/packages/keychain/src/pages/slot/auth/failure.tsx
+++ b/packages/keychain/src/pages/failure.tsx
@@ -1,11 +1,12 @@
+"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 dynamic from "next/dynamic";
import { CARTRIDGE_DISCORD_LINK } from "const";
-function Failure() {
+export default function Failure() {
return (
);
}
-
-export default dynamic(() => Promise.resolve(Failure), { ssr: false });
diff --git a/packages/keychain/src/pages/login.tsx b/packages/keychain/src/pages/login.tsx
index 7657b0f5e..0eb0d832e 100644
--- a/packages/keychain/src/pages/login.tsx
+++ b/packages/keychain/src/pages/login.tsx
@@ -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) {
@@ -22,11 +33,7 @@ function Login() {
return (
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 });
diff --git a/packages/keychain/src/pages/session.tsx b/packages/keychain/src/pages/session.tsx
new file mode 100644
index 000000000..59e10b793
--- /dev/null
+++ b/packages/keychain/src/pages/session.tsx
@@ -0,0 +1,159 @@
+"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";
+import base64url from "base64url";
+
+type SessionQueryParams = Record & {
+ callback_uri?: string;
+ redirect_uri?: string;
+ redirect_query_name?: 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();
+
+ // 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 session = controller.account.sessionJson();
+ if ((!queries.callback_uri && !queries.redirect_uri) || !session) {
+ router.replace(`/failure`);
+ return;
+ }
+
+ const headers = new Headers();
+ headers.append("Content-Type", "application/json");
+
+ const credentialsJson = JSON.stringify({
+ username: controller.username,
+ credentials: {
+ publicKey: controller.publicKey,
+ credentialId: controller.credentialId,
+ },
+ session,
+ });
+
+ if (queries.callback_uri) {
+ fetch(sanitizeCallbackUrl(decodeURIComponent(queries.callback_uri)), {
+ body: credentialsJson,
+ headers,
+ method: "POST",
+ })
+ .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`);
+ });
+ }
+
+ if (queries.redirect_uri) {
+ router.replace(
+ `${decodeURIComponent(queries.redirect_uri)}?${
+ queries.redirect_query_name ?? "session"
+ }=${base64url.encode(credentialsJson)}`,
+ );
+ }
+ }, [
+ router,
+ queries.callback_uri,
+ queries.redirect_uri,
+ queries.redirect_query_name,
+ 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 && !queries.redirect_uri) {
+ throw new Error("Expected either callback_uri or redirect_uri");
+ }
+
+ onCallback();
+ },
+ [queries.callback_uri, queries.redirect_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 ? (
+
+ ) : (
+
+ );
+}
+
+/**
+ * Sanitize the callback url to ensure that it is a valid URL. Returns back the URL.
+ */
+function sanitizeCallbackUrl(url: string): URL | undefined {
+ try {
+ const parsed = new URL(url);
+
+ if (
+ parsed.hostname.endsWith("cartridge.gg") &&
+ parsed.pathname !== "/" &&
+ parsed.pathname !== "/callback"
+ ) {
+ throw new Error(`Invalid callback url: ${url}`);
+ }
+
+ return parsed;
+ } catch (e) {
+ console.error(e);
+ }
+}
diff --git a/packages/keychain/src/pages/signup.tsx b/packages/keychain/src/pages/signup.tsx
index 3615fe6db..6e711eec3 100644
--- a/packages/keychain/src/pages/signup.tsx
+++ b/packages/keychain/src/pages/signup.tsx
@@ -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) {
@@ -22,11 +33,7 @@ function Signup() {
return (
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 });
diff --git a/packages/keychain/src/pages/slot/auth/success.tsx b/packages/keychain/src/pages/slot/auth/success.tsx
deleted file mode 100644
index cba058b10..000000000
--- a/packages/keychain/src/pages/slot/auth/success.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Container } from "components/layout";
-import { SparklesDuoIcon } from "@cartridge/ui";
-import dynamic from "next/dynamic";
-
-function Success() {
- return (
-
- );
-}
-
-export default dynamic(() => Promise.resolve(Success), { ssr: false });
diff --git a/packages/keychain/src/pages/slot/auth/consent.tsx b/packages/keychain/src/pages/slot/consent.tsx
similarity index 100%
rename from packages/keychain/src/pages/slot/auth/consent.tsx
rename to packages/keychain/src/pages/slot/consent.tsx
diff --git a/packages/keychain/src/pages/slot/auth/index.tsx b/packages/keychain/src/pages/slot/index.tsx
similarity index 94%
rename from packages/keychain/src/pages/slot/auth/index.tsx
rename to packages/keychain/src/pages/slot/index.tsx
index cf90fb903..c98a9ccc9 100644
--- a/packages/keychain/src/pages/slot/auth/index.tsx
+++ b/packages/keychain/src/pages/slot/index.tsx
@@ -19,7 +19,7 @@ function Auth() {
"",
);
- router.replace(`/slot/auth/consent${query}`);
+ router.replace(`/slot/consent${query}`);
}
}, [user, controller, router]);
diff --git a/packages/keychain/src/pages/slot/auth/me.graphql b/packages/keychain/src/pages/slot/me.graphql
similarity index 100%
rename from packages/keychain/src/pages/slot/auth/me.graphql
rename to packages/keychain/src/pages/slot/me.graphql
diff --git a/packages/keychain/src/pages/slot/session/index.tsx b/packages/keychain/src/pages/slot/session/index.tsx
deleted file mode 100644
index ddbb2ed42..000000000
--- a/packages/keychain/src/pages/slot/session/index.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import { Policy } from "@cartridge/controller";
-import Controller from "utils/controller";
-import { CreateSession as CreateSessionComp } from "components/connect";
-
-import { fetchAccount } from "components/connect/utils";
-import { useConnection } from "hooks/connection";
-import { useRouter } from "next/router";
-import { useCallback, useEffect, useState } from "react";
-import { PageLoading } from "components/Loading";
-import dynamic from "next/dynamic";
-import { Call, hash } from "starknet";
-
-type SessionQueryParams = Record & {
- callback_uri: string;
- username: string;
-};
-
-/**
- This page is for creating session with Slot
-*/
-function CreateSession() {
- const router = useRouter();
- const queries = router.query as SessionQueryParams;
-
- const { controller, setController, policies, origin, chainId, rpcUrl } =
- useConnection();
-
- // Fetching account status for displaying the loading screen
- const [isFetching, setIsFetching] = useState(true);
-
- // Handler for calling the Slot 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 onSlotCallback = useCallback(() => {
- const url = sanitizeCallbackUrl(decodeURIComponent(queries.callback_uri));
- const session = controller.account.sessionJson();
- if (!url || !session) {
- router.replace(`/slot/auth/failure`);
- return;
- }
-
- const headers = new Headers();
- headers.append("Content-Type", "application/json");
-
- fetch(url, {
- body: JSON.stringify(session),
- headers,
- method: "POST",
- })
- .then(async (res) => {
- return res.status === 200
- ? router.replace(`/slot/auth/success`)
- : new Promise((_, reject) => reject(res));
- })
- .catch((e) => {
- console.error("failed to call the callback url", e);
- router.replace(`/slot/auth/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");
- }
-
- onSlotCallback();
- },
- [queries.callback_uri, controller, onSlotCallback],
- );
-
- // Fetch account details from the username, and create the Controller
- useEffect(() => {
- const username = queries.username;
-
- if (!username || !chainId || !rpcUrl) {
- return;
- }
-
- fetchAccount(username)
- .then((res) => {
- const {
- account: {
- credentials: {
- webauthn: [{ id: credentialId, publicKey }],
- },
- contractAddress: address,
- },
- } = res;
-
- const controller = new Controller({
- appId: origin,
- chainId,
- rpcUrl,
- address,
- username,
- publicKey,
- credentialId,
- });
-
- setController(controller);
- })
- .catch((e) => console.error(e))
- .finally(() => setIsFetching(false));
- }, [rpcUrl, chainId, origin, setController, queries.username]);
-
- // Once the controller is created upon start, 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)) {
- onSlotCallback();
- }
- }, [controller, origin, policies, onSlotCallback]);
-
- // Show loader if currently fetching account
- if (isFetching) {
- return ;
- }
-
- return ;
-}
-
-/**
- * Sanitize the callback url to ensure that it is a valid URL. Returns back the URL.
- */
-function sanitizeCallbackUrl(url: string): URL | undefined {
- try {
- const parsed = new URL(url);
-
- if (parsed.hostname !== "localhost" || parsed.pathname !== "/callback") {
- throw new Error(`Invalid callback url: ${url}`);
- }
-
- return parsed;
- } catch (e) {
- console.error(e);
- }
-}
-
-export default dynamic(() => Promise.resolve(CreateSession), { ssr: false });
diff --git a/packages/keychain/src/pages/success.tsx b/packages/keychain/src/pages/success.tsx
new file mode 100644
index 000000000..67f60d801
--- /dev/null
+++ b/packages/keychain/src/pages/success.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { Container } from "components/layout";
+import { SparklesDuoIcon } from "@cartridge/ui";
+import { useRouter } from "next/router";
+
+export default function Success() {
+ const router = useRouter();
+ const { title, description } = router.query;
+
+ return (
+
+ );
+}
diff --git a/packages/keychain/src/utils/controller.ts b/packages/keychain/src/utils/controller.ts
index 2905db956..75502c4fc 100644
--- a/packages/keychain/src/utils/controller.ts
+++ b/packages/keychain/src/utils/controller.ts
@@ -29,8 +29,8 @@ export default class Controller {
public chainId: string;
public rpcUrl: string;
public signer: SignerInterface;
- protected publicKey: string;
- protected credentialId: string;
+ public publicKey: string;
+ public credentialId: string;
constructor({
appId,