Skip to content

fix: sync Google name and avatar when guest converts to authenticated account#9066

Open
csiyang wants to merge 9 commits intomainfrom
siyangcao/nes-1593-guest-user-name-not-being-updated-when-signing-up-with
Open

fix: sync Google name and avatar when guest converts to authenticated account#9066
csiyang wants to merge 9 commits intomainfrom
siyangcao/nes-1593-guest-user-name-not-being-updated-when-signing-up-with

Conversation

@csiyang
Copy link
Copy Markdown
Contributor

@csiyang csiyang commented Apr 23, 2026

Summary

  • When a guest user links their anonymous account to Google via `linkWithPopup`, their display name and avatar were not appearing in the app after conversion — showing as "Unknown User" with no avatar
  • Root cause: `loginWithCredential` and the pending-journey path in `SignInServiceButton.tsx` both called `getIdToken()` without `forceRefresh: true` after `linkWithPopup`. This meant the session cookie was set with the stale anonymous token (no `name`, `picture`, or `email` claims from Google), so `ctx.currentUser` on the first post-conversion request had no profile data
  • Primary fix (this PR): added `getIdToken(true)` after `linkWithPopup` in both token-acquisition sites so the cookie carries Google's claims immediately
  • Secondary fix (commit `3d669a5a`): extended `findOrFetchUser` Path C to sync `displayName → firstName/lastName`, `email`, and `photoURL → imageUrl` from Firebase Admin when `emailVerified` transitions `false → true` (defence-in-depth against any propagation delay)
  • Tertiary fix (commit `3d669a5a`): `me` resolver fallback now also writes `imageUrl` from JWT claims when converting anonymous → authenticated

Fixes NES-1593

Changes

File Change
`apps/journeys-admin/src/libs/auth/firebase.ts` `getIdToken()` → `getIdToken(true)` in `loginWithCredential`
`apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.tsx` `getIdToken()` → `getIdToken(true)` in the pending-journey path
`apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.spec.tsx` Added test verifying `getIdToken(true)` is called on the pending-journey path
`apis/api-users/src/schema/user/findOrFetchUser.ts` Path C now syncs all Google profile fields on emailVerified transition
`apis/api-users/src/schema/user/user.ts` `me` resolver fallback includes `imageUrl` from JWT claims

Test plan

  • Start as a guest user (enter the customise flow on a template, click next)
  • On the preview/sign-up step, click "Continue with Google"
  • Sign in with a Google account that has a display name and profile picture
  • Verify the user's name shows correctly (not "Unknown User") on the very first page load after conversion
  • Verify the user's avatar shows the Google profile picture immediately

Post-Deploy Monitoring & Validation

  • What to monitor: Check for any increase in "Unknown User" occurrences or missing avatar images in user profiles after deploy
  • Logs: Search for `firstName: 'Unknown User'` in api-users logs; should decrease to zero for new conversions
  • Expected healthy behavior: Guest → Google conversions immediately show Google display name and avatar on first reload
  • Failure signal / rollback trigger: If "Unknown User" persists after conversion on the first load, revert the `getIdToken(true)` change in `firebase.ts` and `SignInServiceButton.tsx`
  • Validation window: 24 hours post-deploy; watch first 10 guest-to-Google conversions

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Profile now syncs email, first/last name and avatar when a user’s emailVerified flips to true.
    • Guest-to-authenticated sign-ins now request a refreshed ID token so profile updates persist immediately.
  • New Features

    • Guest conversion flow propagates the user’s profile image into their account on sign-in.
  • Tests

    • Added tests for email-verified transitions, guest→authenticated image sync, and refreshed-token sign-in flow.
  • Documentation

    • Added a plan detailing the guest→Google profile sync fix and verification steps.

…enticated

When a guest (anonymous) user links their Firebase account to Google via
linkWithPopup, the existing emailVerified sync branch in findOrFetchUser
already calls auth.getUser() — but discarded the displayName, email, and
photoURL from that response. This meant the DB record kept firstName as
"Unknown User" and imageUrl as null after conversion.

Fix: capture and write displayName (parsed into firstName/lastName), email,
and photoURL to the DB in the same update that sets emailVerified: true.

Belt-and-suspenders: also include imageUrl in the me query conversion
branch so any JWT-derived imageUrl is preserved for edge cases where
findOrFetchUser's emailVerified branch doesn't trigger.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented Apr 23, 2026

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Detects when a Firebase user’s emailVerified flips from false to true and re-fetches identity fields from Firebase to update the Prisma user (email, firstName, lastName, imageUrl, emailVerified). Also forces refreshed ID tokens after linking anonymous Google accounts to ensure fresh profile data.

Changes

Cohort / File(s) Summary
Email Verification Sync
apis/api-users/src/schema/user/findOrFetchUser.ts, apis/api-users/src/schema/user/findOrFetchUser.spec.ts
When an existing user's emailVerified is false but Firebase reports true, call auth.getUser, derive firstName/lastName from displayName, normalize email, set imageUrl from photoURL, and update the Prisma user with these fields. Adds a unit test asserting the prismaMock.user.update payload.
Guest-to-Authenticated Conversion
apis/api-users/src/schema/user/user.ts, apis/api-users/src/schema/user/user.spec.ts
Add debug logging for ctx.currentUser and the fetched/existing user’s email. Include ctx.currentUser.imageUrl in the Prisma update during anonymous→authenticated conversion when present. Adds a test verifying data.imageUrl is set from provider payload.
Client ID Token Refresh (auth flows)
apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.tsx, apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.spec.tsx, apps/journeys-admin/src/libs/auth/firebase.ts
After linking anonymous accounts (pending guest journey flow), call getIdToken(true) / credential.user.getIdToken(true) to force a fresh ID token before calling login(idToken). Adds console logging of linked user profile and JWT payload decoding. Tests assert getIdToken(true) is invoked and login receives the refreshed token.
Documentation / Plan
docs/plans/2026-04-23-001-fix-guest-google-profile-sync-plan.md
New plan documenting the guest→Google profile sync issue, proposed client changes (force ID token refresh after linking), tests to add/verify, edge cases, risks, and acceptance criteria.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant UI as SignInServiceButton
    participant AuthClient as Firebase Client
    participant Server as /api/login (me resolver)
    participant AdminAuth as Firebase Admin
    participant DB as Prisma

    Client->>UI: completes Google linkWithPopup
    UI->>AuthClient: credential.user.getIdToken(true)
    AuthClient-->>UI: fresh idToken
    UI->>Server: POST /api/login with idToken

    Server->>AdminAuth: getUserFromPayload / auth.getUser(uid)
    AdminAuth-->>Server: uid, emailVerified, displayName, email, photoURL

    alt existing DB user emailVerified == false and auth.emailVerified == true
        Server->>DB: prisma.user.update(where:{userId}, data:{ emailVerified:true, email, firstName, lastName, imageUrl })
        DB-->>Server: updated user
    else guest -> authenticated with imageUrl
        Server->>DB: prisma.user.update(..., data:{ imageUrl })
        DB-->>Server: updated user
    end

    Server-->>Client: user record / session established
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: syncing Google name and avatar during guest-to-authenticated conversion, which aligns with the primary objective of fixing the missing profile sync issue.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch siyangcao/nes-1593-guest-user-name-not-being-updated-when-signing-up-with

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 23, 2026

View your CI Pipeline Execution ↗ for commit cd14881

Command Status Duration Result
nx run journeys-admin-e2e:e2e ✅ Succeeded 31s View ↗
nx run-many --target=vercel-alias --projects=jo... ✅ Succeeded 2s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 9s View ↗
nx run-many --target=deploy --projects=journeys... ✅ Succeeded 46s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-24 00:42:03 UTC

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
apis/api-users/src/schema/user/findOrFetchUser.ts (1)

33-60: Extract duplicated displayName parsing into a helper.

The nested-ternary parsing here (lines 40-49) duplicates the clearer if/else logic at lines 77-90 in the same file. This is a good opportunity to DRY both branches into a small helper that returns { firstName, lastName } — it also improves readability since the nested ternary is harder to reason about than the equivalent block below.

♻️ Proposed refactor
+function parseDisplayName(displayName?: string | null): {
+  firstName?: string
+  lastName?: string
+} {
+  const parts =
+    displayName?.trim().split(' ').filter((p) => p.length > 0) ?? []
+  if (parts.length === 0) return {}
+  if (parts.length === 1) return { firstName: parts[0] }
+  return {
+    firstName: parts.slice(0, -1).join(' '),
+    lastName: parts[parts.length - 1]
+  }
+}

Then at lines 33-60:

-      const {
-        emailVerified,
-        displayName,
-        email,
-        photoURL
-      } = await auth.getUser(userId)
+      const { emailVerified, displayName, email, photoURL } =
+        await auth.getUser(userId)
       if (emailVerified) {
-        const nameParts =
-          displayName?.trim().split(' ').filter((p) => p.length > 0) ?? []
-        const firstName =
-          nameParts.length >= 1
-            ? nameParts.length === 1
-              ? nameParts[0]
-              : nameParts.slice(0, -1).join(' ')
-            : undefined
-        const lastName =
-          nameParts.length > 1 ? nameParts[nameParts.length - 1] : undefined
-
+        const { firstName, lastName } = parseDisplayName(displayName)
         return await prisma.user.update({

The fallthrough creation branch at lines 73-95 can then reuse the same helper (applying its own 'Unknown User' fallback after the call).

Also worth noting: this branch makes a second auth.getUser(userId) call in addition to the one at line 71 in the creation path. For the verified-flip path that's fine since they're mutually exclusive code paths, just calling it out so it's intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apis/api-users/src/schema/user/findOrFetchUser.ts` around lines 33 - 60, The
displayName parsing logic is duplicated (a nested ternary in the
emailVerified/update path and a clearer if/else in the creation path); extract
that into a small helper (e.g., parseDisplayName(displayName): { firstName?:
string; lastName?: string }) and replace the nested ternary in the auth.getUser
-> prisma.user.update branch with a call to parseDisplayName, and reuse the same
helper in the prisma.user.create branch (apply the existing 'Unknown User'
fallback after calling the helper in the create path); update references in
findOrFetchUser.ts so auth.getUser, prisma.user.update, and prisma.user.create
use the helper and remove the duplicated parsing code.
apis/api-users/src/schema/user/findOrFetchUser.spec.ts (1)

53-85: LGTM.

Test cleanly asserts the new conditional-spread payload (emailVerified + email + firstName + lastName + imageUrl) and the where clause. Good coverage for the happy path of the emailVerified-flip branch.

One optional add for later: a case where displayName is a single token (asserts firstName set, lastName omitted) and one where photoURL/email are null (asserts those keys are absent from data) would lock in the conditional-spread behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apis/api-users/src/schema/user/findOrFetchUser.spec.ts` around lines 53 - 85,
Add two additional unit tests in findOrFetchUser.spec.ts targeting the
findOrFetchUser behavior when emailVerified flips: (1) mock auth.getUser to
return a single-token displayName (e.g., "Cher") and assert
prismaMock.user.update is called with firstName set and lastName omitted; (2)
mock auth.getUser to return null/undefined for photoURL and/or email and assert
prismaMock.user.update's data object omits imageUrl and/or email keys. Use the
same setup pattern as the existing "emailVerified transitions to true" test and
assert the exact payload passed to prismaMock.user.update and the returned value
from findOrFetchUser.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apis/api-users/src/schema/user/findOrFetchUser.spec.ts`:
- Around line 53-85: Add two additional unit tests in findOrFetchUser.spec.ts
targeting the findOrFetchUser behavior when emailVerified flips: (1) mock
auth.getUser to return a single-token displayName (e.g., "Cher") and assert
prismaMock.user.update is called with firstName set and lastName omitted; (2)
mock auth.getUser to return null/undefined for photoURL and/or email and assert
prismaMock.user.update's data object omits imageUrl and/or email keys. Use the
same setup pattern as the existing "emailVerified transitions to true" test and
assert the exact payload passed to prismaMock.user.update and the returned value
from findOrFetchUser.

In `@apis/api-users/src/schema/user/findOrFetchUser.ts`:
- Around line 33-60: The displayName parsing logic is duplicated (a nested
ternary in the emailVerified/update path and a clearer if/else in the creation
path); extract that into a small helper (e.g., parseDisplayName(displayName): {
firstName?: string; lastName?: string }) and replace the nested ternary in the
auth.getUser -> prisma.user.update branch with a call to parseDisplayName, and
reuse the same helper in the prisma.user.create branch (apply the existing
'Unknown User' fallback after calling the helper in the create path); update
references in findOrFetchUser.ts so auth.getUser, prisma.user.update, and
prisma.user.create use the helper and remove the duplicated parsing code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a08dbe78-2552-4df4-9663-136b1fd71f22

📥 Commits

Reviewing files that changed from the base of the PR and between 4cc30b3 and 3d669a5.

📒 Files selected for processing (4)
  • apis/api-users/src/schema/user/findOrFetchUser.spec.ts
  • apis/api-users/src/schema/user/findOrFetchUser.ts
  • apis/api-users/src/schema/user/user.spec.ts
  • apis/api-users/src/schema/user/user.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apis/api-users/src/schema/user/findOrFetchUser.ts`:
- Around line 47-57: The update path for syncing Google display names leaves
lastName undefined for single-part names, so existing lastName is preserved;
change the logic in findOrFetchUser so lastName is set to an empty string for
single-part display names instead of undefined (i.e., compute lastName =
nameParts.length > 1 ? nameParts[nameParts.length - 1] : '' and ensure
prisma.user.update includes lastName unconditionally like the other fields), so
the update clears any stale lastName when only a first name is provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 383bc854-76d1-4aef-806e-6f89aaf37c1f

📥 Commits

Reviewing files that changed from the base of the PR and between 3d669a5 and 9acc22f.

📒 Files selected for processing (1)
  • apis/api-users/src/schema/user/findOrFetchUser.ts

Comment thread apis/api-users/src/schema/user/findOrFetchUser.ts Outdated
@csiyang csiyang self-assigned this Apr 23, 2026
…e syncs immediately

Without forceRefresh:true, getIdToken() returns the cached anonymous token after
linkWithPopup, leaving ctx.currentUser without name/picture/email claims on the
first post-conversion request. The me resolver fallback could not write imageUrl,
and the findOrFetchUser Path C fix was the only safety net (which worked but had
a narrow Firebase Admin propagation race window).

Force-refreshing the token after linkWithPopup guarantees the session cookie carries
Google's name, picture, and email claims, making both the findOrFetchUser sync and
the me resolver fallback reliable on the very first page load after conversion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot requested a deployment to Preview - journeys-admin April 23, 2026 08:12 Pending
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.spec.tsx (1)

311-315: Use the standard journeys-admin test providers for this added render.

This new render only wraps MockedProvider; please use the repo’s standard helper/wrapper or add the required provider stack so the test runs under the same Snackbar and MUI theme context as production.

As per coding guidelines, apps/journeys-admin/**/*.spec.tsx: “Tests must wrap components with required providers: SnackbarProvider → MockedProvider → ThemeProvider”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.spec.tsx`
around lines 311 - 315, The test renders SignInServiceButton only with
MockedProvider but must use the repo's standard provider stack; update the
render call so the component is wrapped inside SnackbarProvider → MockedProvider
→ ThemeProvider (the same helper or wrapper used by other journeys-admin tests)
so SignInServiceButton receives Snackbar and MUI theme context during tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.spec.tsx`:
- Around line 311-315: The test renders SignInServiceButton only with
MockedProvider but must use the repo's standard provider stack; update the
render call so the component is wrapped inside SnackbarProvider → MockedProvider
→ ThemeProvider (the same helper or wrapper used by other journeys-admin tests)
so SignInServiceButton receives Snackbar and MUI theme context during tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 480d36fe-487c-472d-9973-f34c2cbd6439

📥 Commits

Reviewing files that changed from the base of the PR and between 9acc22f and 26eca27.

📒 Files selected for processing (4)
  • apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.spec.tsx
  • apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.tsx
  • apps/journeys-admin/src/libs/auth/firebase.ts
  • docs/plans/2026-04-23-001-fix-guest-google-profile-sync-plan.md
✅ Files skipped from review due to trivial changes (1)
  • apps/journeys-admin/src/libs/auth/firebase.ts

@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin April 23, 2026 08:17 Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plans/2026-04-23-001-fix-guest-google-profile-sync-plan.md`:
- Line 133: Replace the incorrect claim about forcing ID token refresh: remove
or update the sentence referencing "token force-refresh after account linking"
and instead instruct callers to call user.reload() to sync server-side profile
changes and then call getIdToken() (without the force true flag); avoid
recommending getIdToken(true) to prevent unnecessary network calls and quota
use.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1915e5a6-db57-4b9c-a5f1-52780ed26dd8

📥 Commits

Reviewing files that changed from the base of the PR and between 26eca27 and 60ed5e1.

📒 Files selected for processing (1)
  • docs/plans/2026-04-23-001-fix-guest-google-profile-sync-plan.md

- `user.ts` me resolver: `apis/api-users/src/schema/user/user.ts:122`
- `firebase.ts` loginWithCredential: `apps/journeys-admin/src/libs/auth/firebase.ts:28`
- `SignInServiceButton.tsx` pending journey path: `apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.tsx:55`
- Firebase docs: token force-refresh after account linking operations is recommended when the new credential's claims must be reflected immediately
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Firebase Auth documentation: after account linking (e.g., linkWithPopup), is forcing ID token refresh via getIdToken(true) recommended when updated claims (name/picture/email) are needed immediately?

💡 Result:

No, forcing ID token refresh via getIdToken(true) is not generally recommended after account linking with linkWithPopup for immediate access to updated claims like name, picture, or email. The linkWithPopup operation returns a UserCredential with an updated user object (result.user) that reflects the linked providers in providerData. Standard claims (name, picture, email) from the new provider are available in the user's providerData array, where you can access the most relevant one (e.g., the last entry or filter by providerId). The ID token includes these claims based on the user's current profile, which may update to prioritize the primary provider's info after linking, but to ensure the latest server-side profile and token claims immediately, call user.reload first, then getIdToken (without forceRefresh, as it auto-refreshes if needed). getIdToken(true) forces a network refresh regardless of expiration, which can lead to quota issues if overused, as noted in docs and community reports. Official docs on linkWithPopup show accessing result.user directly post-linking without mentioning token refresh needs. For custom claims or guaranteed server sync, reload + getIdToken is the pattern used after server-side changes.

Citations:


Correct the claim about Firebase force-refresh recommendations.

The statement in line 133 contradicts Firebase's actual guidance. Per Firebase documentation, forcing ID token refresh via getIdToken(true) after account linking is not the recommended pattern. Instead, Firebase recommends calling user.reload() first to sync server-side profile updates, then getIdToken() without force refresh. The force parameter should be avoided as it can cause quota issues with unnecessary network calls. Replace line 133 with the correct pattern or remove the unsupported claim entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-04-23-001-fix-guest-google-profile-sync-plan.md` at line 133,
Replace the incorrect claim about forcing ID token refresh: remove or update the
sentence referencing "token force-refresh after account linking" and instead
instruct callers to call user.reload() to sync server-side profile changes and
then call getIdToken() (without the force true flag); avoid recommending
getIdToken(true) to prevent unnecessary network calls and quota use.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the force-refresh intentionally, but scoped to a single call site.

During manual testing we confirmed that after linkWithPopup + updateProfile, a plain getIdToken() returns a cached anonymous token — Firebase auto-refreshes on expiry, not on profile changes. Without force-refresh the JWT sent to Apollo and exchanged by /api/login lacked the name/picture claims, which was the original bug's root cause.

The code now calls getIdToken(true) once per guest-link flow in SignInServiceButton.tsx:70 (one call per sign-in, not per request), so the quota concern doesn't apply. loginWithCredential was reverted to plain getIdToken() for the non-guest path. The plan doc is outdated relative to the shipped code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@csiyang, understood — that's a well-reasoned explanation. You're right that Firebase's token cache is expiry-driven, not event-driven, so getIdToken() without force-refresh can return a stale anonymous token even immediately after linkWithPopup. Scoping the getIdToken(true) call to a single site in the guest-conversion path (once per sign-in, not per request) is the correct tradeoff and addresses the quota concern. I'll drop my objection.


✏️ Learnings added
Learnt from: csiyang
Repo: JesusFilm/core PR: 9066
File: docs/plans/2026-04-23-001-fix-guest-google-profile-sync-plan.md:133-133
Timestamp: 2026-04-24T00:14:43.029Z
Learning: In `apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.tsx`, calling `getIdToken(true)` (force-refresh) after `linkWithPopup` in the guest-conversion path is intentional and required. Manual testing confirmed that after `linkWithPopup` + `updateProfile`, a plain `getIdToken()` returns a cached anonymous token because Firebase only auto-refreshes the token on expiry, not on profile changes or account linking. Without force-refresh, the JWT sent to `/api/login` lacks Google `name`/`picture`/`email` claims, which was the root bug (NES-1593). The call is scoped to the guest-link flow only (once per sign-in), so quota concerns do not apply. `loginWithCredential` in `firebase.ts` uses plain `getIdToken()` for non-guest paths. Do not flag this `getIdToken(true)` call as unnecessary or quota-risky in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 8945
File: apps/journeys-admin/src/components/SignIn/PasswordPage/PasswordPage.tsx:51-53
Timestamp: 2026-04-02T01:24:49.916Z
Learning: In `apps/journeys-admin/src/components/SignIn/PasswordPage/PasswordPage.tsx`, calling `firebaseSignOut(auth)` before `signInWithEmailAndPassword` when `auth.currentUser?.isAnonymous === true` is intentional. Firebase requires the anonymous session to be cleared first because `signInWithEmailAndPassword` cannot run alongside an active anonymous session (that path requires `linkWithCredential`). The `pendingGuestJourney` data in `sessionStorage` persists across the sign-out, so a failed sign-in does not permanently orphan the guest journey — the user can retry. Do not flag this pre-sign-in sign-out as a risk of orphaning pending guest journey data.

Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 8790
File: apps/journeys-admin/pages/journeys/[journeyId]/reports/visitors.tsx:373-376
Timestamp: 2026-03-07T00:23:36.368Z
Learning: In JesusFilm/core (`apps/journeys-admin`), including the Firebase ID token in `userSerialized` passed through `getServerSideProps` props is intentional and required. The token is needed to hydrate authenticated Apollo/GraphQL client state on initial page load. This is safe because: (1) pages using `getServerSideProps` are rendered per-request and never CDN-cached; (2) unauthenticated users are redirected before props are produced; (3) the Firebase ID token is short-lived (1h TTL) and designed for client-side use; (4) `toUser()` already produces a sanitized shape containing only `id`, `email`, `displayName`, `photoURL`, `phoneNumber`, `emailVerified`, and `token` — no refresh tokens or raw claims. Do not flag this pattern as a security issue in future reviews.

Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 8790
File: apps/journeys-admin/pages/teams/new.tsx:88-89
Timestamp: 2026-03-07T00:41:06.607Z
Learning: In JesusFilm/core (`apps/journeys-admin`), do NOT flag `userSerialized: JSON.stringify(user)` in `getServerSideProps` as a security issue even though the `user` object includes a `token` field. Including the Firebase ID token in `userSerialized` is intentional: the token is needed to hydrate authenticated Apollo/GraphQL client state on initial page load. This is safe because pages using `getServerSideProps` are rendered per-request and never CDN-cached; unauthenticated users are redirected before props are produced; the Firebase ID token is short-lived (1h TTL) and designed for client-side use; and `toUser()` already produces a sanitized shape containing only `id`, `email`, `displayName`, `photoURL`, `phoneNumber`, `emailVerified`, and `token` — no refresh tokens or raw claims. This pattern appears consistently across all pages in `apps/journeys-admin/pages/` and must never be flagged as leaking sensitive credentials.

Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 8790
File: apps/journeys-admin/src/libs/initAndAuthApp/initAndAuthApp.ts:66-66
Timestamp: 2026-03-07T02:37:26.434Z
Learning: In `apps/journeys-admin/src/libs/initAndAuthApp/initAndAuthApp.ts`, the `token` assignment `const token = user?.token ?? null` at line 66 does NOT need empty/whitespace normalization. The token originates from `toUser(tokens)` → `tokens.token` via `next-firebase-auth-edge`'s `getTokensFromObject()`, which only returns a `Tokens` object when a cryptographically valid Firebase ID JWT is present; it returns `null` otherwise. That `null` case is already handled upstream by `redirectToLogin` in `getAuthTokens`. A Firebase ID token is a JWT (header.payload.signature) and is structurally non-empty by definition. By the time `initAndAuthApp` is called with a non-null user, the token has already been verified. Do not flag empty-string token normalization as an issue in this file.

Learnt from: CR
Repo: JesusFilm/core PR: 0
File: apps/journeys/AGENTS.md:0-0
Timestamp: 2026-04-20T00:11:11.283Z
Learning: Applies to apps/journeys/src/**/*.{ts,tsx} : For anonymous visitors, use `signInAnonymously` with localStorage persistence to prevent duplicate visitor records

Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 8790
File: apps/journeys-admin/pages/_app.tsx:25-25
Timestamp: 2026-03-07T00:48:42.538Z
Learning: In JesusFilm/core (apps/journeys-admin), calling `getFirebaseAuth()` at module scope in `apps/journeys-admin/pages/_app.tsx` is safe during SSR. The function uses `initializeApp()` and `getAuth()` from Firebase JS SDK v9+, which are isomorphic — the firebase npm package ships both browser and Node.js bundles, and bare initialization does not access browser-only APIs (localStorage, IndexedDB). Those persistence layers are only engaged during actual auth state checks, which occur client-side. Do not flag this pattern as an SSR safety issue.

Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 8814
File: apis/api-journeys-modern/src/schema/journey/adminJourneys.query.ts:37-57
Timestamp: 2026-03-10T22:31:24.117Z
Learning: In `apis/api-journeys-modern/src/schema/journey/adminJourneys.query.ts`, the ordering of the `useLastActiveTeamId` check (Lines 37-44) before the `teamId` check (Line 52), and the behavior of setting `lastActiveApplied = true` even when `profile.lastActiveTeamId` is `null`, is intentional. This is a direct migration of the existing `adminJourneys` query from `api-journeys` to `api-journeys-modern`, and the logic must match the original implementation exactly. Do not flag this ordering or the `lastActiveApplied` flag behavior as a bug in future reviews.

Learnt from: csiyang
Repo: JesusFilm/core PR: 8955
File: apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.ts:39-42
Timestamp: 2026-04-08T02:23:11.723Z
Learning: In `apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.ts`, the escape hatch `if (resolvedUrl.startsWith('/users/verify${encodedRedirect}')) return` (around line 81) is never reached from the verify page itself because `apps/journeys-admin/pages/users/verify.tsx` intentionally omits `resolvedUrl` from `initAndAuthApp`, which prevents `checkConditionalRedirect` from being called for the verify page. The primary loop-prevention for the verify page is the earlier check inside the `emailVerified === false` block: `if (resolvedUrl.startsWith('/users/verify')) return`. Do not flag the line-81 escape hatch as a fragile or broken pattern.

Learnt from: CR
Repo: JesusFilm/core PR: 0
File: apps/journeys/AGENTS.md:0-0
Timestamp: 2026-04-20T00:11:11.283Z
Learning: Applies to apps/journeys/src/**/*.{ts,tsx} : Use Apollo Client with Firebase JWT auth via `setContext` link for all GraphQL requests

Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 9070
File: apis/api-journeys-modern/src/schema/chatButton/chatButtonUpdate.mutation.ts:27-33
Timestamp: 2026-04-23T20:48:34.812Z
Learning: In `apis/api-journeys-modern/src/schema/chatButton/chatButtonUpdate.mutation.ts`, writing `journeyId` into the Prisma `update` data payload (`data: { journeyId, ...input }`) and calling `recalculateJourneyCustomizable(journeyId)` only for the provided `journeyId` is intentional — it is a faithful 1:1 migration of the legacy `apis/api-journeys` `chatButtonUpdate` resolver which has the same behavior. Do not flag the lack of cross-journey ownership validation or the single-journey recalculation call as bugs in future reviews of this file; any hardening should be tracked as a separate improvement issue.

Learnt from: CR
Repo: JesusFilm/core PR: 0
File: apis/AGENTS.md:0-0
Timestamp: 2026-04-01T05:09:07.711Z
Learning: api-gateway: Review changes for header propagation correctness, JWT configuration accuracy, and subgraph naming consistency

Learnt from: mikeallisonJS
Repo: JesusFilm/core PR: 9070
File: apis/api-journeys-modern/src/schema/chatButton/chatButtonCreate.mutation.ts:12-33
Timestamp: 2026-04-23T20:48:46.619Z
Learning: In `apis/api-journeys-modern/src/schema/chatButton/chatButtonCreate.mutation.ts`, the `chatButtonCreate` mutation intentionally allows anonymous users (`isAnonymous: true`) and performs NO journey-ownership or authorization check beyond the 2-button limit. This is a 1:1 faithful port of the legacy `apis/api-journeys/src/app/modules/chatButton/chatButton.resolver.ts` `chatButtonCreate` method, which also has no `UseGuards`, no CASL guard, and no ownership check. Do not flag the missing ownership/auth check on `chatButtonCreate` as a bug in future reviews; adding such a check would be a new feature, not a migration bug fix.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys-admin ✅ Ready journeys-admin preview Fri Apr 24 12:38:28 NZST 2026

@stage-branch-merger
Copy link
Copy Markdown
Contributor

I see you added the "on stage" label, I'll get this merged to the stage branch!

stage-branch-merger Bot added a commit that referenced this pull request Apr 23, 2026
stage-branch-merger Bot added a commit that referenced this pull request Apr 23, 2026
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin April 23, 2026 20:24 Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.tsx`:
- Around line 43-47: Remove the debug console.log lines that print PII in
SignInServiceButton (the logs referencing user.displayName, user.photoURL,
user.email and providerData) and any JWT payload logging added in firebase.ts;
instead either delete these statements or replace them with non-PII-safe
diagnostics (e.g., a single production-guarded debug flag or a
masked/boolean-only log) so no user identifiers are emitted from the
SignInServiceButton component or firebase JWT handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3d029e3b-b826-4326-94e4-8ee7f7ff03af

📥 Commits

Reviewing files that changed from the base of the PR and between 60ed5e1 and ab6034f.

📒 Files selected for processing (4)
  • apis/api-users/src/schema/user/findOrFetchUser.ts
  • apis/api-users/src/schema/user/user.ts
  • apps/journeys-admin/src/components/SignIn/SignInServiceButton/SignInServiceButton.tsx
  • apps/journeys-admin/src/libs/auth/firebase.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apis/api-users/src/schema/user/user.ts
  • apps/journeys-admin/src/libs/auth/firebase.ts
  • apis/api-users/src/schema/user/findOrFetchUser.ts

Addresses multi-agent review of the guest-to-Google profile sync fix:

- Extract splitDisplayName/sanitizeDisplayName/sanitizePhotoURL helpers in
  libs/yoga/src/firebaseClient so JWT-derived and admin-SDK-derived profile
  data are sanitized at a single boundary. Photo URLs must be https; display
  names get control-character stripping and length caps.
- findOrFetchUser: drop the hardcoded provider whitelist, match any linked
  non-firebase provider via providerData; move resolution inside the
  emailVerified branch to avoid wasted work; share split logic with the
  create path; import UserRecord through @core/yoga/firebaseClient.
- SignInServiceButton: wrap updateProfile/reload in try/catch (best-effort,
  server has providerData fallback); force-refresh the ID token once before
  journeyPublish so Apollo attaches fresh name/picture claims; reuse the
  refreshed token for /api/login instead of minting twice; add isSubmitting
  guard to prevent double-click races.
- firebase.ts: revert loginWithCredential to plain getIdToken() — the
  force-refresh only belongs at the guest-link call site.
- Tests: add providerData fallback case, non-whitelisted provider case,
  non-https photoURL rejection, control-char stripping, and
  preserve-existing-name-when-provider-has-none.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stage-branch-merger Bot added a commit that referenced this pull request Apr 24, 2026
@github-actions github-actions Bot requested a deployment to Preview - journeys-admin April 24, 2026 00:15 Pending
stage-branch-merger Bot added a commit that referenced this pull request Apr 24, 2026
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin April 24, 2026 00:20 Inactive
Novel institutional knowledge not previously documented anywhere in the
codebase: Firebase linkWithPopup on an anonymous user does not hoist the
provider's displayName/photoURL onto the top-level user record. The data
lives only in providerData[], which the ID token transform does not read
— so force-refreshing the JWT cannot surface data that isn't there.

Documents:
- Why three prior fix attempts (server-only sync, JWT force-refresh, both
  combined without providerData fallback) each failed
- The two-layer working fix (client-side updateProfile + reload, server
  providerData fallback)
- Prevention patterns and test checklist for future Firebase auth work

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stage-branch-merger Bot added a commit that referenced this pull request Apr 24, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

Warnings
⚠️ ❗ Big PR (751 changes)

(change count - 751): Pull Request size seems relatively large. If Pull Request contains multiple changes, split each into separate PR will helps faster, easier review.

Generated by 🚫 dangerJS against cd14881

@github-actions github-actions Bot requested a deployment to Preview - journeys-admin April 24, 2026 00:32 Pending
stage-branch-merger Bot added a commit that referenced this pull request Apr 24, 2026
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin April 24, 2026 00:36 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants