Skip to content

Commit 6d5e229

Browse files
linnallmoldy530
andauthored
feat: oauth add passkey after signup (redirect flow) (#1140)
* chore: local dev changes updated * chore: wip oauth redirect with add passkey after signup * chore: wip * refactor: remove a few things so we can test redirect * refactor: just render the pop-up on completed login for oauth * refactor: add new user signup event in signer * refactor: add hook to listen to signup events * chore: wip * refactor: use signup event to popup passkey create * fix: the modal was staying open * fix: move setAuthStep in the correct card * fix: infinite subscribe loop * fix: don't open passkey create until logged in * chore: remove local dev callback url * chore: removes unecessary bangs and refactors for readability * chore: removes unused usePrevious hook * fix: update useNewUserSignup to remove listener on dismount and when dependencies change * docs: link related discussion to TODO * refactor: adds emitNewUserEvent wrapper * chore: restore oauth mode to popup * chore: remove logging Co-authored-by: Michael Moldoveanu <michael.moldoveanu@alchemy.com> * chore: remove logging Co-authored-by: Michael Moldoveanu <michael.moldoveanu@alchemy.com> * chore: reset oauth mode to popup * fix: handle undefined isNewUser within emitNewUserEvent wrapper * chore: remove unnecessary bang --------- Co-authored-by: Michael Moldoveanu <michael.moldoveanu@alchemy.com>
1 parent aa3ebf9 commit 6d5e229

File tree

11 files changed

+123
-59
lines changed

11 files changed

+123
-59
lines changed

account-kit/core/src/store/store.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ const addClientSideStoreListeners = (store: Store) => {
273273
}));
274274
});
275275

276+
// TODO: handle this appropriately, see https://github.com/alchemyplatform/aa-sdk/pull/1140#discussion_r1837265706
277+
// signer.on("newUserSignup", () => console.log("got new user signup"));
278+
276279
signer.on("connected", (user) => store.setState({ user }));
277280

278281
signer.on("disconnected", () => {

account-kit/react/src/components/auth/card/index.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"use client";
22

3+
import { disconnect } from "@account-kit/core";
34
import {
45
useCallback,
56
useEffect,
67
useMemo,
78
useRef,
89
type PropsWithChildren,
910
} from "react";
11+
import { useAlchemyAccountContext } from "../../../context.js";
1012
import { useAuthConfig } from "../../../hooks/internal/useAuthConfig.js";
1113
import { useAuthModal } from "../../../hooks/useAuthModal.js";
1214
import { useElementHeight } from "../../../hooks/useElementHeight.js";
@@ -15,9 +17,6 @@ import { Navigation } from "../../navigation.js";
1517
import { useAuthContext } from "../context.js";
1618
import { Footer } from "../sections/Footer.js";
1719
import { Step } from "./steps.js";
18-
import { disconnect } from "@account-kit/core";
19-
import { useAlchemyAccountContext } from "../../../context.js";
20-
2120
export type AuthCardProps = {
2221
className?: string;
2322
};
@@ -58,7 +57,7 @@ export const AuthCardContent = ({
5857
showClose?: boolean;
5958
}) => {
6059
const { openAuthModal, closeAuthModal } = useAuthModal();
61-
const { status, isAuthenticating } = useSignerStatus();
60+
const { status, isAuthenticating, isConnected } = useSignerStatus();
6261
const { authStep, setAuthStep } = useAuthContext();
6362
const { config } = useAlchemyAccountContext();
6463

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

108107
const onClose = useCallback(() => {
109-
// Terminate any inflight authentication
110-
disconnect(config);
108+
if (!isConnected) {
109+
// Terminate any inflight authentication
110+
disconnect(config);
111+
}
111112

112113
if (authStep.type === "passkey_create") {
113114
setAuthStep({ type: "complete" });
114115
} else {
115116
setAuthStep({ type: "initial" });
116117
}
117118
closeAuthModal();
118-
}, [authStep.type, closeAuthModal, setAuthStep, config]);
119+
}, [isConnected, authStep.type, closeAuthModal, config, setAuthStep]);
119120

120121
useEffect(() => {
121122
if (authStep.type === "complete") {
@@ -124,11 +125,6 @@ export const AuthCardContent = ({
124125
onAuthSuccess?.();
125126
} else if (authStep.type !== "initial") {
126127
didGoBack.current = false;
127-
} else if (!didGoBack.current && isAuthenticating) {
128-
setAuthStep({
129-
type: "email_completing",
130-
createPasskeyAfter: addPasskeyOnSignup,
131-
});
132128
}
133129
}, [
134130
authStep,
@@ -139,6 +135,7 @@ export const AuthCardContent = ({
139135
openAuthModal,
140136
closeAuthModal,
141137
addPasskeyOnSignup,
138+
isConnected,
142139
]);
143140

144141
return (

account-kit/react/src/components/auth/card/loading/oauth.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ export const CompletingOAuth = () => {
1313

1414
useEffect(() => {
1515
if (isConnected) {
16-
setAuthStep({ type: "complete" });
16+
if (authStep.createPasskeyAfter) {
17+
setAuthStep({ type: "passkey_create" });
18+
} else {
19+
setAuthStep({ type: "complete" });
20+
}
1721
}
18-
}, [isConnected, setAuthStep]);
22+
}, [authStep.createPasskeyAfter, isConnected, setAuthStep]);
1923

2024
if (authStep.error) {
2125
return (

account-kit/react/src/components/auth/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type AuthStep =
1313
| {
1414
type: "oauth_completing";
1515
config: Extract<AuthType, { type: "social" }>;
16+
createPasskeyAfter?: boolean;
1617
error?: Error;
1718
}
1819
| { type: "initial"; error?: Error }

account-kit/react/src/components/auth/modal.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
1+
import { useCallback } from "react";
2+
import { useNewUserSignup } from "../../hooks/internal/useNewUserSignup.js";
13
import { useAuthModal } from "../../hooks/useAuthModal.js";
4+
import { useSignerStatus } from "../../hooks/useSignerStatus.js";
25
import { useUiConfig } from "../../hooks/useUiConfig.js";
36
import { Dialog } from "../dialog/dialog.js";
47
import { AuthCardContent } from "./card/index.js";
8+
import { useAuthContext } from "./context.js";
59

610
export const AuthModal = () => {
7-
const { modalBaseClassName } = useUiConfig(({ modalBaseClassName }) => ({
8-
modalBaseClassName,
9-
}));
10-
const { isOpen, closeAuthModal } = useAuthModal();
11+
const { isConnected } = useSignerStatus();
12+
const { modalBaseClassName, addPasskeyOnSignup } = useUiConfig(
13+
({ modalBaseClassName, auth }) => ({
14+
modalBaseClassName,
15+
addPasskeyOnSignup: auth?.addPasskeyOnSignup,
16+
})
17+
);
18+
19+
const { setAuthStep } = useAuthContext();
20+
const { isOpen, closeAuthModal, openAuthModal } = useAuthModal();
21+
22+
const handleSignup = useCallback(() => {
23+
if (addPasskeyOnSignup && !isOpen) {
24+
openAuthModal();
25+
setAuthStep({
26+
type: "passkey_create",
27+
});
28+
}
29+
}, [addPasskeyOnSignup, isOpen, openAuthModal, setAuthStep]);
30+
31+
useNewUserSignup(handleSignup, isConnected);
1132

1233
return (
1334
<Dialog isOpen={isOpen} onClose={closeAuthModal}>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useEffect, useRef } from "react";
2+
import { useSigner } from "../useSigner.js";
3+
4+
export function useNewUserSignup(onSignup: () => void, enabled?: boolean) {
5+
const hasHandled = useRef(false);
6+
const signer = useSigner();
7+
8+
useEffect(() => {
9+
if (!enabled) return;
10+
if (!signer) return;
11+
12+
const handleSignup = () => {
13+
if (!hasHandled.current) {
14+
hasHandled.current = true;
15+
onSignup();
16+
}
17+
};
18+
19+
const stopListening = signer.on("newUserSignup", handleSignup);
20+
return stopListening;
21+
}, [enabled, onSignup, signer]);
22+
}

account-kit/react/src/hooks/useUiConfig.tsx

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@
33
import {
44
createContext,
55
useContext,
6-
useEffect,
76
useRef,
87
type PropsWithChildren,
98
} from "react";
109
import { create, useStore, type StoreApi } from "zustand";
1110
import { useShallow } from "zustand/react/shallow";
12-
import { IS_SIGNUP_QP } from "../components/constants.js";
1311
import type {
1412
AlchemyAccountsUIConfig,
1513
AuthIllustrationStyle,
1614
} from "../types.js";
17-
import { useSignerStatus } from "./useSignerStatus.js";
1815

1916
type AlchemyAccountsUIConfigWithDefaults = Omit<
2017
Required<AlchemyAccountsUIConfig>,
@@ -96,31 +93,11 @@ export function UiConfigProvider({
9693
children,
9794
initialConfig,
9895
}: PropsWithChildren<{ initialConfig?: AlchemyAccountsUIConfig }>) {
99-
const { isConnected } = useSignerStatus();
10096
const storeRef = useRef<StoreApi<UiConfigStore>>();
10197
if (!storeRef.current) {
10298
storeRef.current = createUiConfigStore(initialConfig);
10399
}
104100

105-
const { setModalOpen, addPasskeyOnSignup } = useStore(
106-
storeRef.current,
107-
useShallow(({ setModalOpen, auth }) => ({
108-
setModalOpen,
109-
addPasskeyOnSignup: auth?.addPasskeyOnSignup,
110-
}))
111-
);
112-
113-
useEffect(() => {
114-
const urlParams = new URLSearchParams(window.location.search);
115-
if (
116-
isConnected &&
117-
addPasskeyOnSignup &&
118-
urlParams.get(IS_SIGNUP_QP) === "true"
119-
) {
120-
setModalOpen(true);
121-
}
122-
}, [addPasskeyOnSignup, isConnected, setModalOpen]);
123-
124101
return (
125102
<UiConfigContext.Provider value={storeRef.current}>
126103
{children}

account-kit/signer/src/base.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type AlchemySignerStore = {
4646
user: User | null;
4747
status: AlchemySignerStatus;
4848
error: ErrorInfo | null;
49+
isNewUser?: boolean;
4950
};
5051

5152
type InternalStore = Mutate<
@@ -152,6 +153,14 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
152153
),
153154
{ fireImmediately: true }
154155
);
156+
case "newUserSignup":
157+
return this.store.subscribe(
158+
({ isNewUser }) => isNewUser,
159+
(isNewUser) => {
160+
if (isNewUser) (listener as AlchemySignerEvents["newUserSignup"])();
161+
},
162+
{ fireImmediately: true }
163+
);
155164
default:
156165
assertNever(event, `Unknown event type ${event}`);
157166
}
@@ -715,6 +724,9 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
715724
authenticatingType: "email",
716725
});
717726

727+
// fire new user event
728+
this.emitNewUserEvent(params.isNewUser);
729+
718730
return user;
719731
}
720732
};
@@ -777,15 +789,21 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
777789
bundle,
778790
orgId,
779791
idToken,
780-
}: Extract<AuthParams, { type: "oauthReturn" }>): Promise<User> =>
781-
this.inner.completeAuthWithBundle({
792+
isNewUser,
793+
}: Extract<AuthParams, { type: "oauthReturn" }>): Promise<User> => {
794+
const user = this.inner.completeAuthWithBundle({
782795
bundle,
783796
orgId,
784797
connectedEventName: "connectedOauth",
785798
authenticatingType: "oauth",
786799
idToken,
787800
});
788801

802+
this.emitNewUserEvent(isNewUser);
803+
804+
return user;
805+
};
806+
789807
private registerListeners = () => {
790808
this.sessionManager.on("connected", (session) => {
791809
this.store.setState({
@@ -831,6 +849,11 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
831849
});
832850
});
833851
};
852+
853+
private emitNewUserEvent = (isNewUser?: boolean) => {
854+
// assumes that if isNewUser is undefined it is a returning user
855+
if (isNewUser) this.store.setState({ isNewUser });
856+
};
834857
}
835858

836859
function toErrorInfo(error: unknown): ErrorInfo {

account-kit/signer/src/signer.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { SessionManagerParamsSchema } from "./session/manager.js";
99

1010
export type AuthParams =
1111
| { type: "email"; email: string; redirectParams?: URLSearchParams }
12-
| { type: "email"; bundle: string; orgId?: string }
12+
| { type: "email"; bundle: string; orgId?: string; isNewUser?: boolean }
1313
| {
1414
type: "passkey";
1515
email: string;
@@ -36,6 +36,7 @@ export type AuthParams =
3636
bundle: string;
3737
orgId: string;
3838
idToken: string;
39+
isNewUser?: boolean;
3940
};
4041

4142
export type OauthProviderConfig =
@@ -110,16 +111,23 @@ export class AlchemyWebSigner extends BaseAlchemySigner<AlchemySignerWebClient>
110111
} else {
111112
client = params_.client;
112113
}
113-
const { emailBundle, oauthBundle, oauthOrgId, oauthError, idToken } =
114-
getAndRemoveQueryParams({
115-
emailBundle: "bundle",
116-
// We don't need this, but we still want to remove it from the URL.
117-
emailOrgId: "orgId",
118-
oauthBundle: "alchemy-bundle",
119-
oauthOrgId: "alchemy-org-id",
120-
oauthError: "alchemy-error",
121-
idToken: "alchemy-id-token",
122-
});
114+
const {
115+
emailBundle,
116+
oauthBundle,
117+
oauthOrgId,
118+
oauthError,
119+
idToken,
120+
isSignup,
121+
} = getAndRemoveQueryParams({
122+
emailBundle: "bundle",
123+
// We don't need this, but we still want to remove it from the URL.
124+
emailOrgId: "orgId",
125+
oauthBundle: "alchemy-bundle",
126+
oauthOrgId: "alchemy-org-id",
127+
oauthError: "alchemy-error",
128+
idToken: "alchemy-id-token",
129+
isSignup: "aa-is-signup",
130+
});
123131

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

129137
super({ client, sessionConfig, initialError });
130138

139+
const isNewUser = isSignup === "true";
140+
131141
if (emailBundle) {
132-
this.authenticate({ type: "email", bundle: emailBundle });
142+
this.authenticate({
143+
type: "email",
144+
bundle: emailBundle,
145+
isNewUser,
146+
});
133147
} else if (oauthBundle && oauthOrgId && idToken) {
134148
this.authenticate({
135149
type: "oauthReturn",
136150
bundle: oauthBundle,
137151
orgId: oauthOrgId,
138152
idToken,
153+
isNewUser,
139154
});
140155
}
141156
}

account-kit/signer/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { User } from "./client/types";
22

33
export type AlchemySignerEvents = {
44
connected(user: User): void;
5+
newUserSignup(): void;
56
disconnected(): void;
67
statusChanged(status: AlchemySignerStatus): void;
78
errorChanged(error: ErrorInfo | undefined): void;

0 commit comments

Comments
 (0)