Skip to content

Fix Auth.js session callback & jwt update() so avatar/name/email alwa…#391

Open
okeolaolatun23-glitch wants to merge 1 commit into
geevapp:mainfrom
okeolaolatun23-glitch:Auth.js-session-callback-ove-writes-session.user-and-the-jwt-callback-ignores-update-avatar-name-email-never
Open

Fix Auth.js session callback & jwt update() so avatar/name/email alwa…#391
okeolaolatun23-glitch wants to merge 1 commit into
geevapp:mainfrom
okeolaolatun23-glitch:Auth.js-session-callback-ove-writes-session.user-and-the-jwt-callback-ignores-update-avatar-name-email-never

Conversation

@okeolaolatun23-glitch

Copy link
Copy Markdown

Closes #345

Summary

Two bugs in lib/auth-config.ts (and one in components/avatar-upload.tsx) combined to make avatar updates a silent no-op: the session callback replaced session.user entirely instead of spreading it, and the jwt callback never handled trigger === "update", so updateSession() calls from the client were completely ignored. This PR fixes both callbacks and the upload component so that profile changes (avatar, name, email) reflect immediately in all useSession() consumers without requiring a full page reload.


Files modified

File Change
app/lib/auth-config.ts Fixed jwt and session callbacks
app/components/avatar-upload.tsx Fixed updateSession() call shape

Root cause analysis

1. session callback replaced session.user instead of spreading it

Before

async session({ session, token }) {
  if (token) {
    (session.user as any) = {          // ← overwrites the entire object
      id: token.id as string,
      walletAddress: token.walletAddress,
      username: token.username,
    };
  }
  return session;
}

session.user.name, .email, and .image were discarded, so any component reading those fields (nav avatar, display name, etc.) received undefined.

After

async session({ session, token }) {
  if (token) {
    session.user = {
      ...session.user,                  // ← preserve standard fields
      name:  token.name  ?? session.user?.name  ?? null,
      email: token.email ?? session.user?.email ?? null,
      image: (token.picture as string) ?? session.user?.image ?? null,
      // custom Geev fields
      id:            token.id as string,
      walletAddress: token.walletAddress ?? null,
      username:      token.username      ?? null,
    };
  }
  return session;
}

2. jwt callback ignored trigger === "update"

Before

async jwt({ token, user }) {
  if (user) {
    token.id            = user.id;
    token.walletAddress = user.walletAddress;
    token.username      = user.username;
  }
  // trigger and session parameters never read
  return token;
}

Auth.js calls the jwt callback with trigger: "update" and a session argument whenever updateSession() is called client-side. Because the callback never inspected those parameters, every update was silently discarded.

After

async jwt({ token, user, trigger, session }) {
  // Initial sign-in
  if (user) {
    token.id            = user.id;
    token.walletAddress = user.walletAddress ?? null;
    token.username      = user.username      ?? null;
    token.picture       = user.image         ?? token.picture ?? null;
  }

// Client-side session.update() / updateSession()
if (trigger === "update" && session?.user) {
const u = session.user;
if (u.image !== undefined) token.picture = u.image;
if (u.name !== undefined) token.name = u.name;
if (u.email !== undefined) token.email = u.email;
if (u.walletAddress !== undefined) token.walletAddress = u.walletAddress;
if (u.username !== undefined) token.username = u.username;
}

return token;
}

Only fields that are explicitly present in the update payload are merged, so a partial update (e.g. only image) does not accidentally wipe name or email.

3. avatar-upload.tsx sent the wrong payload shape

Before

await updateSession({ user: { ...session.user, image: item.url } });

Spreading session.user here inlined custom fields (id, walletAddress, …) at the user level, which did match the expected shape. However because the jwt callback didn't read them at all, the result was still a no-op.

After

await updateSession({ user: { image: item.url } });

Only the changed field is sent. The jwt callback's trigger === "update" branch selectively merges it into the token, and the session callback maps token.picturesession.user.image. All consumers update without a reload.


Auth.js callback data flow (corrected)

Client: updateSession({ user: { image: newUrl } })
        ↓
jwt callback (trigger="update", session.user.image = newUrl)
  → token.picture = newUrl
        ↓
session callback (token.picture = newUrl)
  → session.user.image = newUrl
        ↓
useSession().data.user.image === newUrl  ✓  (no reload needed)

TypeScript module augmentation

The PR also adds explicit declare module "next-auth" and declare module "next-auth/jwt" blocks so that session.user.walletAddress, session.user.username, token.walletAddress, and token.username are all typed — eliminating the (session.user as any) casts that previously masked the bug.


Testing checklist

  • Unit — jwt callback, initial sign-in: user object provided → token.id, token.walletAddress, token.username, token.picture are all set.
  • Unit — jwt callback, update trigger: trigger === "update" with session.user.imagetoken.picture updated; other token fields unchanged.
  • Unit — session callback: session.user.name, .email, .image are present in the returned session alongside id, walletAddress, username.
  • Integration — avatar upload: upload a new image → useSession().data.user.image reflects the new URL in the same browser session without window.location.reload().
  • Integration — nav avatar: after updateSession(), the nav bar avatar src updates without a full reload.
  • Regression — sign-in: existing sign-in flow still sets id, walletAddress, username on the session.
  • Regression — partial updates: calling updateSession({ user: { image } }) does not nullify name or email.

References

# PR #345 — Fix Auth.js session callback & jwt `update()` so avatar/name/email always reflect

Summary

Two bugs in lib/auth-config.ts (and one in components/avatar-upload.tsx) combined to make avatar updates a silent no-op: the session callback replaced session.user entirely instead of spreading it, and the jwt callback never handled trigger === "update", so updateSession() calls from the client were completely ignored. This PR fixes both callbacks and the upload component so that profile changes (avatar, name, email) reflect immediately in all useSession() consumers without requiring a full page reload.


Files modified

File Change
app/lib/auth-config.ts Fixed jwt and session callbacks
app/components/avatar-upload.tsx Fixed updateSession() call shape

Root cause analysis

1. session callback replaced session.user instead of spreading it

Before

async session({ session, token }) {
  if (token) {
    (session.user as any) = {          // ← overwrites the entire object
      id: token.id as string,
      walletAddress: token.walletAddress,
      username: token.username,
    };
  }
  return session;
}

session.user.name, .email, and .image were discarded, so any component reading those fields (nav avatar, display name, etc.) received undefined.

After

async session({ session, token }) {
  if (token) {
    session.user = {
      ...session.user,                  // ← preserve standard fields
      name:  token.name  ?? session.user?.name  ?? null,
      email: token.email ?? session.user?.email ?? null,
      image: (token.picture as string) ?? session.user?.image ?? null,
      // custom Geev fields
      id:            token.id as string,
      walletAddress: token.walletAddress ?? null,
      username:      token.username      ?? null,
    };
  }
  return session;
}

2. jwt callback ignored trigger === "update"

Before

async jwt({ token, user }) {
  if (user) {
    token.id            = user.id;
    token.walletAddress = user.walletAddress;
    token.username      = user.username;
  }
  // trigger and session parameters never read
  return token;
}

Auth.js calls the jwt callback with trigger: "update" and a session argument whenever updateSession() is called client-side. Because the callback never inspected those parameters, every update was silently discarded.

After

async jwt({ token, user, trigger, session }) {
  // Initial sign-in
  if (user) {
    token.id            = user.id;
    token.walletAddress = user.walletAddress ?? null;
    token.username      = user.username      ?? null;
    token.picture       = user.image         ?? token.picture ?? null;
  }

  // Client-side session.update() / updateSession()
  if (trigger === "update" && session?.user) {
    const u = session.user;
    if (u.image         !== undefined) token.picture       = u.image;
    if (u.name          !== undefined) token.name          = u.name;
    if (u.email         !== undefined) token.email         = u.email;
    if (u.walletAddress !== undefined) token.walletAddress = u.walletAddress;
    if (u.username      !== undefined) token.username      = u.username;
  }

  return token;
}

Only fields that are explicitly present in the update payload are merged, so a partial update (e.g. only image) does not accidentally wipe name or email.

3. avatar-upload.tsx sent the wrong payload shape

Before

await updateSession({ user: { ...session.user, image: item.url } });

Spreading session.user here inlined custom fields (id, walletAddress, …) at the user level, which did match the expected shape. However because the jwt callback didn't read them at all, the result was still a no-op.

After

await updateSession({ user: { image: item.url } });

Only the changed field is sent. The jwt callback's trigger === "update" branch selectively merges it into the token, and the session callback maps token.picturesession.user.image. All consumers update without a reload.


Auth.js callback data flow (corrected)

Client: updateSession({ user: { image: newUrl } })
        ↓
jwt callback (trigger="update", session.user.image = newUrl)
  → token.picture = newUrl
        ↓
session callback (token.picture = newUrl)
  → session.user.image = newUrl
        ↓
useSession().data.user.image === newUrl  ✓  (no reload needed)

TypeScript module augmentation

The PR also adds explicit declare module "next-auth" and declare module "next-auth/jwt" blocks so that session.user.walletAddress, session.user.username, token.walletAddress, and token.username are all typed — eliminating the (session.user as any) casts that previously masked the bug.


Testing checklist

  • Unit — jwt callback, initial sign-in: user object provided → token.id, token.walletAddress, token.username, token.picture are all set.
  • Unit — jwt callback, update trigger: trigger === "update" with session.user.imagetoken.picture updated; other token fields unchanged.
  • Unit — session callback: session.user.name, .email, .image are present in the returned session alongside id, walletAddress, username.
  • Integration — avatar upload: upload a new image → useSession().data.user.image reflects the new URL in the same browser session without window.location.reload().
  • Integration — nav avatar: after updateSession(), the nav bar avatar src updates without a full reload.
  • Regression — sign-in: existing sign-in flow still sets id, walletAddress, username on the session.
  • Regression — partial updates: calling updateSession({ user: { image } }) does not nullify name or email.

@drips-wave

drips-wave Bot commented Jun 27, 2026

Copy link
Copy Markdown

@okeolaolatun23-glitch Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@3m1n3nc3

Copy link
Copy Markdown
Contributor

Please resolve the CI failures

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auth.js session callback overwrites session.user and the jwt callback ignores update() — avatar/name/email never reflect

2 participants