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

feat: oauth add passkey after signup (redirect flow) #1140

Merged
merged 36 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3882dca
chore: local dev changes updated
linnall Oct 30, 2024
8ab130a
Merge branch 'main' into social-auth-dev
linnall Oct 31, 2024
ca9e5fd
chore: merge in main
linnall Nov 1, 2024
0a63891
chore: wip oauth redirect with add passkey after signup
linnall Nov 4, 2024
f62b9c8
Merge branch 'main' into social-auth-dev
linnall Nov 5, 2024
8b42698
Merge branch 'main' into social-auth-dev
linnall Nov 8, 2024
bf02b3c
chore: wip
linnall Nov 8, 2024
5d5951a
refactor: remove a few things so we can test redirect
moldy530 Nov 8, 2024
ee46248
refactor: just render the pop-up on completed login for oauth
moldy530 Nov 8, 2024
13b1156
refactor: add new user signup event in signer
moldy530 Nov 8, 2024
1101154
refactor: add hook to listen to signup events
moldy530 Nov 8, 2024
c3aa3bf
chore: wip
linnall Nov 8, 2024
a559ab7
refactor: use signup event to popup passkey create
moldy530 Nov 8, 2024
f4a9d94
fix: the modal was staying open
moldy530 Nov 8, 2024
9b504a3
fix: move setAuthStep in the correct card
moldy530 Nov 8, 2024
0b592c0
fix: infinite subscribe loop
moldy530 Nov 8, 2024
d2c862c
fix: don't open passkey create until logged in
moldy530 Nov 8, 2024
979442b
Merge branch 'main' into social-auth-dev
linnall Nov 8, 2024
998463e
chore: remove local dev callback url
linnall Nov 8, 2024
1e04872
Merge branch 'main' into linna/oauth-add-passkey
linnall Nov 11, 2024
85edb03
chore: removes unecessary bangs and refactors for readability
linnall Nov 11, 2024
a853832
chore: removes unused usePrevious hook
linnall Nov 12, 2024
195149e
Merge branch 'main' into linna/oauth-add-passkey
linnall Nov 12, 2024
f061538
Merge branch 'main' into linna/oauth-add-passkey
linnall Nov 12, 2024
6060220
fix: update useNewUserSignup to remove listener on dismount and when …
linnall Nov 12, 2024
c0e796a
docs: link related discussion to TODO
linnall Nov 12, 2024
e1dda2f
refactor: adds emitNewUserEvent wrapper
linnall Nov 12, 2024
8d10d70
chore: restore oauth mode to popup
linnall Nov 12, 2024
46f694e
Merge branch 'main' into linna/oauth-add-passkey
linnall Nov 12, 2024
78e37ed
chore: remove logging
linnall Nov 13, 2024
1975bdd
chore: remove logging
linnall Nov 13, 2024
d952810
chore: reset oauth mode to popup
linnall Nov 13, 2024
640a7de
fix: handle undefined isNewUser within emitNewUserEvent wrapper
linnall Nov 13, 2024
1adcbd7
chore: remove unnecessary bang
linnall Nov 13, 2024
49309d8
Merge branch 'main' into linna/oauth-add-passkey
linnall Nov 13, 2024
d5c0299
Merge branch 'main' into linna/oauth-add-passkey
linnall Nov 13, 2024
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
3 changes: 3 additions & 0 deletions account-kit/core/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ const addClientSideStoreListeners = (store: Store) => {
}));
});

// TODO: handle this appropriately, see https://github.com/alchemyplatform/aa-sdk/pull/1140#discussion_r1837265706
// signer.on("newUserSignup", () => console.log("got new user signup"));

signer.on("connected", (user) => store.setState({ user }));

signer.on("disconnected", () => {
Expand Down
21 changes: 9 additions & 12 deletions account-kit/react/src/components/auth/card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"use client";

import { disconnect } from "@account-kit/core";
import {
useCallback,
useEffect,
useMemo,
useRef,
type PropsWithChildren,
} from "react";
import { useAlchemyAccountContext } from "../../../context.js";
import { useAuthConfig } from "../../../hooks/internal/useAuthConfig.js";
import { useAuthModal } from "../../../hooks/useAuthModal.js";
import { useElementHeight } from "../../../hooks/useElementHeight.js";
Expand All @@ -15,9 +17,6 @@ import { Navigation } from "../../navigation.js";
import { useAuthContext } from "../context.js";
import { Footer } from "../sections/Footer.js";
import { Step } from "./steps.js";
import { disconnect } from "@account-kit/core";
import { useAlchemyAccountContext } from "../../../context.js";

export type AuthCardProps = {
className?: string;
};
Expand Down Expand Up @@ -58,7 +57,7 @@ export const AuthCardContent = ({
showClose?: boolean;
}) => {
const { openAuthModal, closeAuthModal } = useAuthModal();
const { status, isAuthenticating } = useSignerStatus();
const { status, isAuthenticating, isConnected } = useSignerStatus();
const { authStep, setAuthStep } = useAuthContext();
const { config } = useAlchemyAccountContext();

Expand Down Expand Up @@ -106,16 +105,18 @@ export const AuthCardContent = ({
}, [authStep, setAuthStep, config]);

const onClose = useCallback(() => {
// Terminate any inflight authentication
disconnect(config);
if (!isConnected) {
// Terminate any inflight authentication
disconnect(config);
}

if (authStep.type === "passkey_create") {
setAuthStep({ type: "complete" });
} else {
setAuthStep({ type: "initial" });
}
closeAuthModal();
}, [authStep.type, closeAuthModal, setAuthStep, config]);
}, [isConnected, authStep.type, closeAuthModal, config, setAuthStep]);

useEffect(() => {
if (authStep.type === "complete") {
Expand All @@ -124,11 +125,6 @@ export const AuthCardContent = ({
onAuthSuccess?.();
} else if (authStep.type !== "initial") {
didGoBack.current = false;
} else if (!didGoBack.current && isAuthenticating) {
setAuthStep({
type: "email_completing",
createPasskeyAfter: addPasskeyOnSignup,
});
}
}, [
authStep,
Expand All @@ -139,6 +135,7 @@ export const AuthCardContent = ({
openAuthModal,
closeAuthModal,
addPasskeyOnSignup,
isConnected,
]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ export const CompletingOAuth = () => {

useEffect(() => {
if (isConnected) {
setAuthStep({ type: "complete" });
if (authStep.createPasskeyAfter) {
setAuthStep({ type: "passkey_create" });
} else {
setAuthStep({ type: "complete" });
}
}
}, [isConnected, setAuthStep]);
}, [authStep.createPasskeyAfter, isConnected, setAuthStep]);

if (authStep.error) {
return (
Expand Down
1 change: 1 addition & 0 deletions account-kit/react/src/components/auth/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type AuthStep =
| {
type: "oauth_completing";
config: Extract<AuthType, { type: "social" }>;
createPasskeyAfter?: boolean;
error?: Error;
}
| { type: "initial"; error?: Error }
Expand Down
29 changes: 25 additions & 4 deletions account-kit/react/src/components/auth/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import { useCallback } from "react";
import { useNewUserSignup } from "../../hooks/internal/useNewUserSignup.js";
import { useAuthModal } from "../../hooks/useAuthModal.js";
import { useSignerStatus } from "../../hooks/useSignerStatus.js";
import { useUiConfig } from "../../hooks/useUiConfig.js";
import { Dialog } from "../dialog/dialog.js";
import { AuthCardContent } from "./card/index.js";
import { useAuthContext } from "./context.js";

export const AuthModal = () => {
const { modalBaseClassName } = useUiConfig(({ modalBaseClassName }) => ({
modalBaseClassName,
}));
const { isOpen, closeAuthModal } = useAuthModal();
const { isConnected } = useSignerStatus();
const { modalBaseClassName, addPasskeyOnSignup } = useUiConfig(
({ modalBaseClassName, auth }) => ({
modalBaseClassName,
addPasskeyOnSignup: auth?.addPasskeyOnSignup,
})
);

const { setAuthStep } = useAuthContext();
const { isOpen, closeAuthModal, openAuthModal } = useAuthModal();

const handleSignup = useCallback(() => {
if (addPasskeyOnSignup && !isOpen) {
openAuthModal();
setAuthStep({
type: "passkey_create",
});
}
}, [addPasskeyOnSignup, isOpen, openAuthModal, setAuthStep]);

useNewUserSignup(handleSignup, isConnected);

return (
<Dialog isOpen={isOpen} onClose={closeAuthModal}>
Expand Down
22 changes: 22 additions & 0 deletions account-kit/react/src/hooks/internal/useNewUserSignup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useRef } from "react";
import { useSigner } from "../useSigner.js";

export function useNewUserSignup(onSignup: () => void, enabled?: boolean) {
const hasHandled = useRef(false);
const signer = useSigner();

useEffect(() => {
if (!enabled) return;
if (!signer) return;

const handleSignup = () => {
if (!hasHandled.current) {
hasHandled.current = true;
onSignup();
}
};

const stopListening = signer.on("newUserSignup", handleSignup);
return stopListening;
}, [enabled, onSignup, signer]);
}
23 changes: 0 additions & 23 deletions account-kit/react/src/hooks/useUiConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
import {
createContext,
useContext,
useEffect,
useRef,
type PropsWithChildren,
} from "react";
import { create, useStore, type StoreApi } from "zustand";
import { useShallow } from "zustand/react/shallow";
import { IS_SIGNUP_QP } from "../components/constants.js";
import type {
AlchemyAccountsUIConfig,
AuthIllustrationStyle,
} from "../types.js";
import { useSignerStatus } from "./useSignerStatus.js";

type AlchemyAccountsUIConfigWithDefaults = Omit<
Required<AlchemyAccountsUIConfig>,
Expand Down Expand Up @@ -96,31 +93,11 @@ export function UiConfigProvider({
children,
initialConfig,
}: PropsWithChildren<{ initialConfig?: AlchemyAccountsUIConfig }>) {
const { isConnected } = useSignerStatus();
const storeRef = useRef<StoreApi<UiConfigStore>>();
if (!storeRef.current) {
storeRef.current = createUiConfigStore(initialConfig);
}

const { setModalOpen, addPasskeyOnSignup } = useStore(
storeRef.current,
useShallow(({ setModalOpen, auth }) => ({
setModalOpen,
addPasskeyOnSignup: auth?.addPasskeyOnSignup,
}))
);

useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (
isConnected &&
addPasskeyOnSignup &&
urlParams.get(IS_SIGNUP_QP) === "true"
) {
setModalOpen(true);
}
}, [addPasskeyOnSignup, isConnected, setModalOpen]);

return (
<UiConfigContext.Provider value={storeRef.current}>
{children}
Expand Down
27 changes: 25 additions & 2 deletions account-kit/signer/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type AlchemySignerStore = {
user: User | null;
status: AlchemySignerStatus;
error: ErrorInfo | null;
isNewUser?: boolean;
};

type InternalStore = Mutate<
Expand Down Expand Up @@ -152,6 +153,14 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
),
{ fireImmediately: true }
);
case "newUserSignup":
return this.store.subscribe(
({ isNewUser }) => isNewUser,
(isNewUser) => {
if (isNewUser) (listener as AlchemySignerEvents["newUserSignup"])();
},
{ fireImmediately: true }
);
default:
assertNever(event, `Unknown event type ${event}`);
}
Expand Down Expand Up @@ -715,6 +724,9 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
authenticatingType: "email",
});

// fire new user event
this.emitNewUserEvent(params.isNewUser);

return user;
}
};
Expand Down Expand Up @@ -777,15 +789,21 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
bundle,
orgId,
idToken,
}: Extract<AuthParams, { type: "oauthReturn" }>): Promise<User> =>
this.inner.completeAuthWithBundle({
isNewUser,
}: Extract<AuthParams, { type: "oauthReturn" }>): Promise<User> => {
const user = this.inner.completeAuthWithBundle({
bundle,
orgId,
connectedEventName: "connectedOauth",
authenticatingType: "oauth",
idToken,
});

this.emitNewUserEvent(isNewUser);

return user;
};

private registerListeners = () => {
this.sessionManager.on("connected", (session) => {
this.store.setState({
Expand Down Expand Up @@ -831,6 +849,11 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
});
});
};

private emitNewUserEvent = (isNewUser?: boolean) => {
// assumes that if isNewUser is undefined it is a returning user
if (isNewUser) this.store.setState({ isNewUser });
};
}

function toErrorInfo(error: unknown): ErrorInfo {
Expand Down
39 changes: 27 additions & 12 deletions account-kit/signer/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SessionManagerParamsSchema } from "./session/manager.js";

export type AuthParams =
| { type: "email"; email: string; redirectParams?: URLSearchParams }
| { type: "email"; bundle: string; orgId?: string }
| { type: "email"; bundle: string; orgId?: string; isNewUser?: boolean }
| {
type: "passkey";
email: string;
Expand All @@ -36,6 +36,7 @@ export type AuthParams =
bundle: string;
orgId: string;
idToken: string;
isNewUser?: boolean;
};

export type OauthProviderConfig =
Expand Down Expand Up @@ -110,16 +111,23 @@ export class AlchemyWebSigner extends BaseAlchemySigner<AlchemySignerWebClient>
} else {
client = params_.client;
}
const { emailBundle, oauthBundle, oauthOrgId, oauthError, idToken } =
getAndRemoveQueryParams({
emailBundle: "bundle",
// We don't need this, but we still want to remove it from the URL.
emailOrgId: "orgId",
oauthBundle: "alchemy-bundle",
oauthOrgId: "alchemy-org-id",
oauthError: "alchemy-error",
idToken: "alchemy-id-token",
});
const {
emailBundle,
oauthBundle,
oauthOrgId,
oauthError,
idToken,
isSignup,
} = getAndRemoveQueryParams({
emailBundle: "bundle",
// We don't need this, but we still want to remove it from the URL.
emailOrgId: "orgId",
oauthBundle: "alchemy-bundle",
oauthOrgId: "alchemy-org-id",
oauthError: "alchemy-error",
idToken: "alchemy-id-token",
isSignup: "aa-is-signup",
});

const initialError =
oauthError != null
Expand All @@ -128,14 +136,21 @@ export class AlchemyWebSigner extends BaseAlchemySigner<AlchemySignerWebClient>

super({ client, sessionConfig, initialError });

const isNewUser = isSignup === "true";

if (emailBundle) {
this.authenticate({ type: "email", bundle: emailBundle });
this.authenticate({
type: "email",
bundle: emailBundle,
isNewUser,
});
} else if (oauthBundle && oauthOrgId && idToken) {
this.authenticate({
type: "oauthReturn",
bundle: oauthBundle,
orgId: oauthOrgId,
idToken,
isNewUser,
});
}
}
Expand Down
1 change: 1 addition & 0 deletions account-kit/signer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { User } from "./client/types";

export type AlchemySignerEvents = {
connected(user: User): void;
newUserSignup(): void;
disconnected(): void;
statusChanged(status: AlchemySignerStatus): void;
errorChanged(error: ErrorInfo | undefined): void;
Expand Down
8 changes: 4 additions & 4 deletions examples/ui-demo/src/hooks/useBreakpoint.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useLayoutEffect, useState } from "react";

// Define the breakpoints based on Tailwind CSS defaults (or your custom Tailwind configuration)
const breakpoints = {
Expand All @@ -16,10 +16,10 @@ type Breakpoint = keyof typeof breakpoints;
*
* @returns {Breakpoint} The current active breakpoint
*/
export const useBreakpoint = (): Breakpoint => {
const [currentBreakpoint, setCurrentBreakpoint] = useState<Breakpoint>("sm");
export const useBreakpoint = (): Breakpoint | undefined => {
const [currentBreakpoint, setCurrentBreakpoint] = useState<Breakpoint>();
linnall marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
useLayoutEffect(() => {
const getBreakpoint = (width: number): Breakpoint => {
if (width >= breakpoints["2xl"]) return "2xl";
if (width >= breakpoints.xl) return "xl";
Expand Down
Loading