Repository: alchemyplatform/aa-sdk
Pull request #1441: feat: pull email mode from server
Author: login: florrdv
Created: 2025-03-12T17:50:07Z | Merged: 2025-03-17T17:38:31Z

# Pull Request Checklist

- [ ] Did you add new tests and confirm existing tests pass? (`yarn test`)
- [ ] Did you update relevant docs? (docs are found in the `site` folder, and guidelines for updating/adding docs can be found in the [contribution guide](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md))
- [ ] Do your commits follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] Does your PR title also follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] If you have a breaking change, is it [correctly reflected in your commit message](https://www.conventionalcommits.org/en/v1.0.0/#examples)? (e.g. `feat!: breaking change`)
- [ ] Did you run lint (`yarn lint:check`) and fix any issues? (`yarn lint:write`)
- [ ] Did you follow the [contribution guidelines](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md)?

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on deprecating the `emailMode` option in the authentication configuration, simplifying the email authentication process, and introducing a new configuration management for the signer.

### Detailed summary
- Removed `emailMode` from email authentication sections.
- Deprecated `emailMode` in various types with a warning for future removal.
- Introduced `SignerConfig` and `EmailConfig` types for better configuration management.
- Updated `EmailAuth` component to handle new configuration logic.
- Added methods for fetching signer configuration.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->


Diff:
diff --git a/account-kit/react/src/components/auth/sections/EmailAuth.tsx b/account-kit/react/src/components/auth/sections/EmailAuth.tsx
index 8b335bb084..e8a077e02c 100644
--- a/account-kit/react/src/components/auth/sections/EmailAuth.tsx
+++ b/account-kit/react/src/components/auth/sections/EmailAuth.tsx
@@ -19,7 +19,7 @@ type EmailAuthProps = Extract<AuthType, { type: "email" }>;
 // eslint-disable-next-line jsdoc/require-jsdoc
 export const EmailAuth = memo(
   ({
-    emailMode = "otp",
+    emailMode: legacyEmailMode,
     hideButton = false,
     buttonLabel = ls.login.email.button,
     placeholder = ls.login.email.placeholder,
@@ -28,8 +28,15 @@ export const EmailAuth = memo(
     const signer = useSigner();
     const { authenticateAsync, isPending } = useAuthenticate({
       onMutate: async (params) => {
+        const cfg = await signer?.getConfig();
         if (params.type === "email" && "email" in params) {
-          if (params.emailMode === "magicLink") {
+          const emailMode = cfg?.email.mode
+            ? cfg?.email.mode
+            : params.emailMode === "magicLink"
+            ? "MAGIC_LINK"
+            : "OTP";
+
+          if (emailMode === "MAGIC_LINK") {
             setAuthStep({ type: "email_verify", email: params.email });
           } else {
             setAuthStep({ type: "otp_verify", email: params.email });
@@ -61,7 +68,7 @@ export const EmailAuth = memo(
           await authenticateAsync({
             type: "email",
             email,
-            emailMode,
+            emailMode: legacyEmailMode,
             redirectParams,
           });
         } catch (e) {
diff --git a/account-kit/react/src/components/auth/types.ts b/account-kit/react/src/components/auth/types.ts
index 12aa08a474..91a3ca6a32 100644
--- a/account-kit/react/src/components/auth/types.ts
+++ b/account-kit/react/src/components/auth/types.ts
@@ -9,6 +9,7 @@ export type AuthType =
   | {
       // TODO: this should support setting redirectParams which will be added to the email redirect
       type: "email";
+      /** @deprecated This option will be overriden by dashboard settings. Please use the dashboard settings instead. This option will be removed in a future release. */
       emailMode?: "magicLink" | "otp";
       hideButton?: boolean;
       buttonLabel?: string;
diff --git a/account-kit/signer/src/base.ts b/account-kit/signer/src/base.ts
index 8caec0b73d..0caeb7bc92 100644
--- a/account-kit/signer/src/base.ts
+++ b/account-kit/signer/src/base.ts
@@ -64,6 +64,14 @@ type InternalStore = Mutate<
   [["zustand/subscribeWithSelector", never]]
 >;
 
+export type EmailConfig = {
+  mode?: "MAGIC_LINK" | "OTP";
+};
+
+export type SignerConfig = {
+  email: EmailConfig;
+};
+
 /**
  * Base abstract class for Alchemy Signer, providing authentication and session management for smart accounts.
  * Implements the `SmartAccountAuthenticator` interface and handles various signer events.
@@ -75,6 +83,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
   inner: TClient;
   private sessionManager: SessionManager;
   private store: InternalStore;
+  private config: Promise<SignerConfig>;
 
   /**
    * Initializes an instance with the provided client and session configuration.
@@ -113,6 +122,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
     this.registerListeners();
     // then initialize so that we can catch those events
     this.sessionManager.initialize();
+    this.config = this.fetchConfig();
   }
 
   /**
@@ -1011,6 +1021,28 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
     // assumes that if isNewUser is undefined it is a returning user
     if (isNewUser) this.store.setState({ isNewUser });
   };
+
+  protected initConfig = async (): Promise<SignerConfig> => {
+    this.config = this.fetchConfig();
+    return this.config;
+  };
+
+  /**
+   * Returns the signer configuration while fetching it if it's not already initialized.
+   *
+   * @returns {Promise<SignerConfig>} A promise that resolves to the signer configuration
+   */
+  public getConfig = async (): Promise<SignerConfig> => {
+    if (!this.config) {
+      return this.initConfig();
+    }
+
+    return this.config;
+  };
+
+  protected fetchConfig = async (): Promise<SignerConfig> => {
+    return this.inner.request("/v1/signer-config", {});
+  };
 }
 
 function toErrorInfo(error: unknown): ErrorInfo {
diff --git a/account-kit/signer/src/client/types.ts b/account-kit/signer/src/client/types.ts
index b3dc17774f..e7d398168d 100644
--- a/account-kit/signer/src/client/types.ts
+++ b/account-kit/signer/src/client/types.ts
@@ -29,6 +29,7 @@ export type CreateAccountParams =
   | {
       type: "email";
       email: string;
+      /** @deprecated This option will be overriden by dashboard settings. Please use the dashboard settings instead. This option will be removed in a future release. */
       emailMode?: EmailType;
       expirationSeconds?: number;
       redirectParams?: URLSearchParams;
@@ -48,6 +49,7 @@ export type EmailType = "magicLink" | "otp";
 
 export type EmailAuthParams = {
   email: string;
+  /** @deprecated This option will be overriden by dashboard settings. Please use the dashboard settings instead. This option will be removed in a future release. */
   emailMode?: EmailType;
   expirationSeconds?: number;
   targetPublicKey: string;
@@ -79,6 +81,14 @@ export type OauthConfig = {
   authProviders: AuthProviderConfig[];
 };
 
+export type EmailConfig = {
+  mode?: "MAGIC_LINK" | "OTP";
+};
+
+export type SignerConfig = {
+  email: EmailConfig;
+};
+
 export type AuthProviderConfig = {
   id: string;
   isCustomProvider?: boolean;
@@ -157,6 +167,11 @@ export type SignerEndpoints = [
     Route: "/v1/otp";
     Body: OtpParams;
     Response: { credentialBundle: string };
+  },
+  {
+    Route: "/v1/signer-config";
+    Body: {};
+    Response: SignerConfig;
   }
 ];
 
diff --git a/account-kit/signer/src/signer.ts b/account-kit/signer/src/signer.ts
index c67da91d62..e16c8498b6 100644
--- a/account-kit/signer/src/signer.ts
+++ b/account-kit/signer/src/signer.ts
@@ -11,6 +11,7 @@ export type AuthParams =
   | {
       type: "email";
       email: string;
+      /** @deprecated This option will be overriden by dashboard settings. Please use the dashboard settings instead. This option will be removed in a future release. */
       emailMode?: "magicLink" | "otp";
       redirectParams?: URLSearchParams;
     }
diff --git a/examples/ui-demo/src/app/config.tsx b/examples/ui-demo/src/app/config.tsx
index 8b797b4ea7..96726712f0 100644
--- a/examples/ui-demo/src/app/config.tsx
+++ b/examples/ui-demo/src/app/config.tsx
@@ -107,7 +107,7 @@ export const alchemyConfig = () =>
       illustrationStyle: DEFAULT_CONFIG.ui.illustrationStyle,
       auth: {
         sections: [
-          [{ type: "email", emailMode: "otp" }],
+          [{ type: "email" }],
           [
             { type: "passkey" },
             { type: "social", authProviderId: "google", mode: "popup" },
diff --git a/examples/ui-demo/src/app/sections.ts b/examples/ui-demo/src/app/sections.ts
index 398ea16f76..76c8ec3fe0 100644
--- a/examples/ui-demo/src/app/sections.ts
+++ b/examples/ui-demo/src/app/sections.ts
@@ -20,7 +20,7 @@ export function getSectionsForConfig(
   const sections: AuthType[][] = [];
   const midSection: AuthType[] = [];
   if (showEmail) {
-    sections.push([{ type: "email", emailMode: "otp" }]);
+    sections.push([{ type: "email" }]);
   }
   if (showPasskey) {
     midSection.push({ type: "passkey" });
diff --git a/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/getConfig.mdx b/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/getConfig.mdx
new file mode 100644
index 0000000000..9fa601e8dc
--- /dev/null
+++ b/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/getConfig.mdx
@@ -0,0 +1,20 @@
+---
+# This file is autogenerated
+title: getConfig
+description: Overview of the getConfig method
+---
+
+# getConfig
+
+Returns the signer configuration while fetching it if it's not already initialized.
+
+## Import
+
+```ts
+import { BaseAlchemySigner } from "@account-kit/signer";
+```
+
+## Returns
+
+`Promise<SignerConfig>`
+A promise that resolves to the signer configuration


Reviews:
user: login: jakehobbs
body: lgtm, is the signer service ready to handle this in prod? also what about the switch b/w the two email modes on the demo app?
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T20:23:29Z
user: login: moldy530
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T20:27:13Z
user: login: moldy530
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T20:27:36Z
user: login: moldy530
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T20:29:44Z
user: login: moldy530
body: LGTM mostly nits. I'd approve if it wasn't gonna get dismissed when you rebase lol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T20:30:24Z
user: login: florrdv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T20:33:56Z
user: login: florrdv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:18:08Z
user: login: florrdv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:18:54Z
user: login: moldy530
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T16:26:24Z
user: login: moldy530
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T16:26:50Z
user: login: jakehobbs
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-17T17:24:34Z

Review comments:
diff_hunk: @@ -75,6 +83,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
   inner: TClient;
   private sessionManager: SessionManager;
   private store: InternalStore;
+  private config: SignerConfig | undefined;
path: account-kit/signer/src/base.ts
user: login: moldy530
body: should this instead just be `Promise<SignerConfig>`? we await it anyways in the auth components
created_at: 2025-03-12T20:27:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,6 +83,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
   inner: TClient;
   private sessionManager: SessionManager;
   private store: InternalStore;
+  private config: SignerConfig | undefined;
path: account-kit/signer/src/base.ts
user: login: moldy530
body: that way it's never null since that doesn't seem like a state it should be in anyways
created_at: 2025-03-12T20:27:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,8 +27,9 @@ export const EmailAuth = memo(
     const signer = useSigner();
     const { authenticateAsync, isPending } = useAuthenticate({
       onMutate: async (params) => {
+        const cfg = await signer?.getConfig();
         if (params.type === "email" && "email" in params) {
-          if (params.emailMode === "magicLink") {
+          if (cfg?.email.mode === "MAGIC_LINK") {
path: account-kit/react/src/components/auth/sections/EmailAuth.tsx
user: login: moldy530
body: we might want to fallback to the user provided one if it exists here still, at least for backwards compat. 

If it's set in the dashboard, then we should ignore the user provided one here (and elsewhere)
created_at: 2025-03-12T20:29:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,8 +27,9 @@ export const EmailAuth = memo(
     const signer = useSigner();
     const { authenticateAsync, isPending } = useAuthenticate({
       onMutate: async (params) => {
+        const cfg = await signer?.getConfig();
         if (params.type === "email" && "email" in params) {
-          if (params.emailMode === "magicLink") {
+          if (cfg?.email.mode === "MAGIC_LINK") {
path: account-kit/react/src/components/auth/sections/EmailAuth.tsx
user: login: florrdv
body: Can do! We can remove all that logic next major version then. Let me make those changes
created_at: 2025-03-12T20:33:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,6 +83,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
   inner: TClient;
   private sessionManager: SessionManager;
   private store: InternalStore;
+  private config: SignerConfig | undefined;
path: account-kit/signer/src/base.ts
user: login: florrdv
body: fixed!
created_at: 2025-03-12T21:18:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,6 +122,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
     this.registerListeners();
     // then initialize so that we can catch those events
     this.sessionManager.initialize();
+    this.config = this.fetchConfig();
path: account-kit/signer/src/base.ts
user: login: florrdv
body: TS can't infer that we set config here if we use initConfig sadly
created_at: 2025-03-12T21:18:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,8 +28,15 @@ export const EmailAuth = memo(
     const signer = useSigner();
     const { authenticateAsync, isPending } = useAuthenticate({
       onMutate: async (params) => {
+        const cfg = await signer?.getConfig();
         if (params.type === "email" && "email" in params) {
-          if (params.emailMode === "magicLink") {
+          const emailMode = legacyEmailMode
+            ? legacyEmailMode === "magicLink"
+              ? "MAGIC_LINK"
+              : "OTP"
+            : cfg?.email.mode;
path: account-kit/react/src/components/auth/sections/EmailAuth.tsx
user: login: moldy530
body: nit: I would probably swap the order of priority here to this since if its present on the server and legacyMode doesn't match you're gonna get an error anyways when you call the server

```suggestion
          const emailMode = cfg?.email.mode ? cfg.email.mode : legacyEmailMode
            ? legacyEmailMode === "magicLink"
              ? "MAGIC_LINK"
              : "OTP";
```
created_at: 2025-03-13T16:26:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,6 +122,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
     this.registerListeners();
     // then initialize so that we can catch those events
     this.sessionManager.initialize();
+    this.config = this.fetchConfig();
path: account-kit/signer/src/base.ts
user: login: moldy530
body: oh interesting
created_at: 2025-03-13T16:26:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-12T17:50:12Z
body: [vc]: #lfV+AZhj/5pCAfbm0cno9Tz53Sh9Y0DIWTHrHYdqR4w=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhYS1zZGstc2l0ZSIsInJvb3REaXJlY3RvcnkiOiJzaXRlIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FsY2hlbXktZG90LWNvbS9hYS1zZGstc2l0ZS80SEZURjNTU2VBeFI2R3JORXNiU3NDclZaVlI0IiwicHJldmlld1VybCI6ImFhLXNkay1zaXRlLWdpdC1mbG9yLWRlcHJlY2F0ZS1lbWFpbC1tb2RlLWNsZWFuLWFsY2hlbXktZG90LWNvbS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImFhLXNkay1zaXRlLWdpdC1mbG9yLWRlcHJlY2F0ZS1lbWFpbC1tb2RlLWNsZWFuLWFsY2hlbXktZG90LWNvbS52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImFhLXNkay11aS1kZW1vIiwicm9vdERpcmVjdG9yeSI6ImV4YW1wbGVzL3VpLWRlbW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYWxjaGVteS1kb3QtY29tL2FhLXNkay11aS1kZW1vL0dRRFFZUWFNV0RvR0NQZnNXVGVQWHFUV2s5RWMiLCJwcmV2aWV3VXJsIjoiYWEtc2RrLXVpLWRlbW8tZ2l0LWZsb3ItZGVwcmVjYXRlLWVtYWlsLWExZDQyZS1hbGNoZW15LWRvdC1jb20udmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJhYS1zZGstdWktZGVtby1naXQtZmxvci1kZXByZWNhdGUtZW1haWwtYTFkNDJlLWFsY2hlbXktZG90LWNvbS52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **aa-sdk-site** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-site/4HFTF3SSeAxR6GrNEsbSsCrVZVR4)) | [Visit Preview](https://aa-sdk-site-git-flor-deprecate-email-mode-clean-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-site-git-flor-deprecate-email-mode-clean-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | Mar 17, 2025 5:32pm |
| **aa-sdk-ui-demo** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-ui-demo/GQDQYQaMWDoGCPfsWTePXqTWk9Ec)) | [Visit Preview](https://aa-sdk-ui-demo-git-flor-deprecate-email-a1d42e-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-ui-demo-git-flor-deprecate-email-a1d42e-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | Mar 17, 2025 5:32pm |



author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-03-12T17:50:14Z
body: ## How to use the Graphite Merge Queue

Add the label <kbd>graphite-merge-queue</kbd> to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/alchemyplatform?ref=merge-queue-instructions-comment&prId=5986289003).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: CONTRIBUTOR
user: login: florrdv
created_at: 2025-03-12T21:18:00Z
body: > lgtm, is the signer service ready to handle this in prod? also what about the switch b/w the two email modes on the demo app?

pending signer upgrade to deploy. discussed with will & we won't be adding support to demo app to avoid confusion for now at least! have another PR up to add those changes if we ever want to go back on that 
author_association: CONTRIBUTOR