fix(stripe): getCustomerCreateParams not actually being called (#5019)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Ebalo
2025-10-02 05:09:08 +02:00
committed by GitHub
parent 8b13d387bd
commit 13872bea56
5 changed files with 274 additions and 15 deletions

View File

@@ -43,6 +43,7 @@
}
},
"dependencies": {
"defu": "^6.1.4",
"zod": "^4.1.5"
},
"peerDependencies": {

View File

@@ -26,6 +26,7 @@ import type {
} from "./types";
import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils";
import { getSchema } from "./schema";
import { defu } from "defu";
const STRIPE_ERROR_CODES = {
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
@@ -1299,13 +1300,27 @@ export const stripe = <O extends StripeOptions>(options: O) => {
create: {
async after(user, ctx) {
if (ctx && options.createCustomerOnSignUp) {
const stripeCustomer = await client.customers.create({
let extraCreateParams: Partial<Stripe.CustomerCreateParams> =
{};
if (options.getCustomerCreateParams) {
extraCreateParams = await options.getCustomerCreateParams(
user,
ctx,
);
}
const params: Stripe.CustomerCreateParams = defu(
{
email: user.email,
name: user.name,
metadata: {
userId: user.id,
},
});
},
extraCreateParams,
);
const stripeCustomer =
await client.customers.create(params);
await ctx.context.internalAdapter.updateUser(user.id, {
stripeCustomerId: stripeCustomer.id,
});

View File

@@ -1496,4 +1496,247 @@ describe("stripe", async () => {
email: "newemail@example.com",
});
});
describe("getCustomerCreateParams", () => {
it("should call getCustomerCreateParams and merge with default params", async () => {
const getCustomerCreateParamsMock = vi
.fn()
.mockResolvedValue({ metadata: { customField: "customValue" } });
const testOptions = {
...stripeOptions,
createCustomerOnSignUp: true,
getCustomerCreateParams: getCustomerCreateParamsMock,
} satisfies StripeOptions;
const testAuth = betterAuth({
database: memory,
baseURL: "http://localhost:3000",
emailAndPassword: {
enabled: true,
},
plugins: [stripe(testOptions)],
});
const testAuthClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [bearer(), stripeClient({ subscription: true })],
fetchOptions: {
customFetchImpl: async (url, init) =>
testAuth.handler(new Request(url, init)),
},
});
// Sign up a user
const userRes = await testAuthClient.signUp.email(
{
email: "custom-params@email.com",
password: "password",
name: "Custom User",
},
{
throw: true,
},
);
// Verify getCustomerCreateParams was called
expect(getCustomerCreateParamsMock).toHaveBeenCalledWith(
expect.objectContaining({
id: userRes.user.id,
email: "custom-params@email.com",
name: "Custom User",
}),
expect.objectContaining({
context: expect.any(Object),
}),
);
// Verify customer was created with merged params
expect(mockStripe.customers.create).toHaveBeenCalledWith(
expect.objectContaining({
email: "custom-params@email.com",
name: "Custom User",
metadata: expect.objectContaining({
userId: userRes.user.id,
customField: "customValue",
}),
}),
);
});
it("should use getCustomerCreateParams to add custom address", async () => {
const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
address: {
line1: "123 Main St",
city: "San Francisco",
state: "CA",
postal_code: "94111",
country: "US",
},
});
const testOptions = {
...stripeOptions,
createCustomerOnSignUp: true,
getCustomerCreateParams: getCustomerCreateParamsMock,
} satisfies StripeOptions;
const testAuth = betterAuth({
database: memory,
baseURL: "http://localhost:3000",
emailAndPassword: {
enabled: true,
},
plugins: [stripe(testOptions)],
});
const testAuthClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [bearer(), stripeClient({ subscription: true })],
fetchOptions: {
customFetchImpl: async (url, init) =>
testAuth.handler(new Request(url, init)),
},
});
// Sign up a user
await testAuthClient.signUp.email(
{
email: "address-user@email.com",
password: "password",
name: "Address User",
},
{
throw: true,
},
);
// Verify customer was created with address
expect(mockStripe.customers.create).toHaveBeenCalledWith(
expect.objectContaining({
email: "address-user@email.com",
name: "Address User",
address: {
line1: "123 Main St",
city: "San Francisco",
state: "CA",
postal_code: "94111",
country: "US",
},
metadata: expect.objectContaining({
userId: expect.any(String),
}),
}),
);
});
it("should properly merge nested objects using defu", async () => {
const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
metadata: {
customField: "customValue",
anotherField: "anotherValue",
},
phone: "+1234567890",
});
const testOptions = {
...stripeOptions,
createCustomerOnSignUp: true,
getCustomerCreateParams: getCustomerCreateParamsMock,
} satisfies StripeOptions;
const testAuth = betterAuth({
database: memory,
baseURL: "http://localhost:3000",
emailAndPassword: {
enabled: true,
},
plugins: [stripe(testOptions)],
});
const testAuthClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [bearer(), stripeClient({ subscription: true })],
fetchOptions: {
customFetchImpl: async (url, init) =>
testAuth.handler(new Request(url, init)),
},
});
// Sign up a user
const userRes = await testAuthClient.signUp.email(
{
email: "merge-test@email.com",
password: "password",
name: "Merge User",
},
{
throw: true,
},
);
// Verify customer was created with properly merged params
// defu merges objects and preserves all fields
expect(mockStripe.customers.create).toHaveBeenCalledWith(
expect.objectContaining({
email: "merge-test@email.com",
name: "Merge User",
phone: "+1234567890",
metadata: {
userId: userRes.user.id,
customField: "customValue",
anotherField: "anotherValue",
},
}),
);
});
it("should work without getCustomerCreateParams", async () => {
// This test ensures backward compatibility
const testOptions = {
...stripeOptions,
createCustomerOnSignUp: true,
// No getCustomerCreateParams provided
} satisfies StripeOptions;
const testAuth = betterAuth({
database: memory,
baseURL: "http://localhost:3000",
emailAndPassword: {
enabled: true,
},
plugins: [stripe(testOptions)],
});
const testAuthClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [bearer(), stripeClient({ subscription: true })],
fetchOptions: {
customFetchImpl: async (url, init) =>
testAuth.handler(new Request(url, init)),
},
});
// Sign up a user
const userRes = await testAuthClient.signUp.email(
{
email: "no-custom-params@email.com",
password: "password",
name: "Default User",
},
{
throw: true,
},
);
// Verify customer was created with default params only
expect(mockStripe.customers.create).toHaveBeenCalledWith({
email: "no-custom-params@email.com",
name: "Default User",
metadata: {
userId: userRes.user.id,
},
});
});
});
});

View File

@@ -199,12 +199,9 @@ export interface StripeOptions {
* @returns
*/
getCustomerCreateParams?: (
data: {
user: User;
session: Session;
},
user: User,
ctx: GenericEndpointContext,
) => Promise<{}>;
) => Promise<Partial<Stripe.CustomerCreateParams>>;
/**
* Subscriptions
*/

11
pnpm-lock.yaml generated
View File

@@ -1154,6 +1154,9 @@ importers:
packages/stripe:
dependencies:
defu:
specifier: ^6.1.4
version: 6.1.4
zod:
specifier: ^4.1.5
version: 4.1.5
@@ -14935,7 +14938,7 @@ snapshots:
postcss: 8.4.49
resolve-from: 5.0.0
optionalDependencies:
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -15020,7 +15023,7 @@ snapshots:
'@expo/json-file': 10.0.7
'@react-native/normalize-colors': 0.81.4
debug: 4.4.1
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
resolve-from: 5.0.0
semver: 7.7.2
xml2js: 0.6.0
@@ -19179,7 +19182,7 @@ snapshots:
resolve-from: 5.0.0
optionalDependencies:
'@babel/runtime': 7.28.4
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
transitivePeerDependencies:
- '@babel/core'
- supports-color
@@ -20741,7 +20744,7 @@ snapshots:
expo-keep-awake@15.0.7(expo@54.0.10)(react@19.1.1):
dependencies:
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
expo: 54.0.10(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.8)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)
react: 19.1.1
expo-linking@7.1.7(expo@54.0.10)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1):