feat(phone-number): support user additionalFields in signUpOnVerification flow (#7699)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Taesu
2026-01-30 15:34:23 +09:00
committed by GitHub
parent 2d62ac0c9a
commit 9e1ab8e6da
3 changed files with 121 additions and 36 deletions

View File

@@ -135,6 +135,16 @@ export const auth = betterAuth({
We highly recommend not awaiting the `sendOTP` function. If you await it, it'll slow down the request and could cause timing attacks. For serverless platforms, you can use `waitUntil` to ensure the OTP is sent.
</Callout>
If you have additional required fields in your user schema, you can pass them in the verify request body:
```ts title="auth-client.ts"
await authClient.phoneNumber.verify({
phoneNumber: "+1234567890",
code: "123456",
customField: "custom-value", // additional field [!code highlight]
})
```
### Sign In with Phone Number
In addition to signing in a user using send-verify flow, you can also use phone number as an identifier and sign in a user using phone number and password.

View File

@@ -594,6 +594,65 @@ describe("updateUser phone number update prevention", async () => {
});
});
describe("signUpOnVerification with additionalFields", async () => {
let otp = "";
const { client } = await getTestInstance(
{
user: {
additionalFields: {
lastName: {
type: "string",
required: true,
},
dateOfBirth: {
type: "date",
required: false,
},
},
},
plugins: [
phoneNumber({
async sendOTP({ code }) {
otp = code;
},
signUpOnVerification: {
getTempEmail(phoneNumber) {
return `temp-${phoneNumber}@example.com`;
},
},
}),
],
},
{
disableTestUser: true,
clientOptions: {
plugins: [phoneNumberClient()],
},
},
);
const testPhoneNumber = "+1234567890";
it("should create user with additional fields from body", async () => {
await client.phoneNumber.sendOtp({
phoneNumber: testPhoneNumber,
});
const res = await client.phoneNumber.verify({
phoneNumber: testPhoneNumber,
code: otp,
lastName: "Doe",
});
expect(res.error).toBe(null);
expect(res.data?.status).toBe(true);
expect(res.data?.user).not.toBe(null);
// @ts-expect-error - additionalFields not yet inferred in plugin response type
expect(res.data?.user.lastName).toBe("Doe");
});
});
describe("custom verifyOTP", async () => {
const mockVerifyOTP = vi.fn();

View File

@@ -4,6 +4,7 @@ import * as z from "zod";
import { getSessionFromCtx } from "../../api";
import { setSessionCookie } from "../../cookies";
import { generateRandomString } from "../../crypto/random";
import { parseUserInput } from "../../db";
import { parseUserOutput } from "../../db/schema";
import type { User } from "../../types";
import { getDate } from "../../utils/date";
@@ -292,42 +293,44 @@ export const sendPhoneNumberOTP = (opts: RequiredPhoneNumberOptions) =>
},
);
const verifyPhoneNumberBodySchema = z.object({
/**
* Phone number
*/
phoneNumber: z.string().meta({
description: 'Phone number to verify. Eg: "+1234567890"',
}),
/**
* OTP code
*/
code: z.string().meta({
description: 'OTP code. Eg: "123456"',
}),
/**
* Disable session creation after verification
* @default false
*/
disableSession: z
.boolean()
.meta({
description: "Disable session creation after verification. Eg: false",
})
.optional(),
/**
* This checks if there is a session already
* and updates the phone number with the provided
* phone number
*/
updatePhoneNumber: z
.boolean()
.meta({
description:
"Check if there is a session and update the phone number. Eg: true",
})
.optional(),
});
const verifyPhoneNumberBodySchema = z
.object({
/**
* Phone number
*/
phoneNumber: z.string().meta({
description: 'Phone number to verify. Eg: "+1234567890"',
}),
/**
* OTP code
*/
code: z.string().meta({
description: 'OTP code. Eg: "123456"',
}),
/**
* Disable session creation after verification
* @default false
*/
disableSession: z
.boolean()
.meta({
description: "Disable session creation after verification. Eg: false",
})
.optional(),
/**
* This checks if there is a session already
* and updates the phone number with the provided
* phone number
*/
updatePhoneNumber: z
.boolean()
.meta({
description:
"Check if there is a session and update the phone number. Eg: true",
})
.optional(),
})
.and(z.record(z.string(), z.any()));
/**
* ### Endpoint
@@ -559,8 +562,21 @@ export const verifyPhoneNumber = (opts: RequiredPhoneNumberOptions) =>
});
if (!user) {
if (opts?.signUpOnVerification) {
const {
phoneNumber,
code,
disableSession,
updatePhoneNumber,
...rest
} = ctx.body;
const additionalFields = parseUserInput(
ctx.context.options,
rest,
"create",
);
user =
await ctx.context.internalAdapter.createUser<UserWithPhoneNumber>({
...additionalFields,
email: opts.signUpOnVerification.getTempEmail(
ctx.body.phoneNumber,
),