fix(store): roll back profile and email changes the auth server rejects

updateProfile, updateEmail and sendVerificationEmail awaited the
better-auth client, which resolves with an error instead of throwing.
On mobile a rejected handle or email kept the new value in the local
user and database and showed a success toast, and a failed verification
email reported as sent. They now throw on an auth error, so the
optimistic change rolls back and callers show the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DIYgod
2026-10-12 01:43:10 +08:00
parent 7dcd00ac46
commit 67aa847285
2 changed files with 44 additions and 4 deletions
@@ -1,7 +1,8 @@
import type { AuthClient } from "@follow/shared/auth"
import { FollowAPIError, FollowAuthError, FollowTimeoutError } from "@follow-app/client-sdk"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { apiContext } from "../../context"
import { apiContext, authClientContext } from "../../context"
import type { FollowAPI } from "../../types"
import type { MeModel } from "./store"
import { userSyncService, useUserStore } from "./store"
@@ -57,3 +58,32 @@ describe("userSyncService.whoami", () => {
expect(useUserStore.getState().whoami).toBeNull()
})
})
describe("userSyncService.updateProfile", () => {
const updateUserMock = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
useUserStore.setState({ users: {}, whoami: me, role: null, roleEndAt: null })
authClientContext.provide({ updateUser: updateUserMock } as unknown as AuthClient)
})
it("rolls the change back when the auth server rejects it", async () => {
updateUserMock.mockResolvedValue({
data: null,
error: { message: "Handle is already taken", status: 400, statusText: "Bad Request" },
})
await expect(userSyncService.updateProfile({ handle: "taken" })).rejects.toThrow(
"Handle is already taken",
)
expect(useUserStore.getState().whoami).toEqual(me)
})
it("keeps the change when the auth server accepts it", async () => {
updateUserMock.mockResolvedValue({ data: { status: true }, error: null })
await userSyncService.updateProfile({ handle: "mine" })
expect(useUserStore.getState().whoami).toEqual({ ...me, handle: "mine" })
})
})
@@ -119,10 +119,14 @@ class UserSyncService {
})
tx.request(async () => {
await authClient().updateUser({
const res = await authClient().updateUser({
...data,
socialLinks: (data.socialLinks || null) as any,
})
// The auth client resolves with the error instead of throwing; throw so the change rolls back
if (res.error) {
throw new Error(res.error.message || res.error.statusText)
}
})
tx.persist(async () => {
const { whoami } = get()
@@ -145,7 +149,10 @@ class UserSyncService {
async sendVerificationEmail() {
const me = get().whoami
if (!me?.email) return
await authClient().sendVerificationEmail({ email: me.email! })
const res = await authClient().sendVerificationEmail({ email: me.email! })
if (res.error) {
throw new Error(res.error.message || res.error.statusText)
}
}
async updateTwoFactor(enabled: boolean, password: string) {
@@ -182,7 +189,10 @@ class UserSyncService {
tx.request(async () => {
const { whoami } = get()
if (!whoami) return
await authClient().changeEmail({ newEmail: email })
const res = await authClient().changeEmail({ newEmail: email })
if (res.error) {
throw new Error(res.error.message || res.error.statusText)
}
})
tx.rollback(() => {
immerSet((state) => {