Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9272bc2
payouts tab
BilalG1 Dec 13, 2025
1782237
cancel subscription endpoint
BilalG1 Dec 15, 2025
9b16230
billing info and payment method update
BilalG1 Dec 16, 2025
94cb95d
only set payment method
BilalG1 Dec 16, 2025
1104e6f
simplify
BilalG1 Dec 16, 2025
a42a359
fix tests
BilalG1 Dec 16, 2025
56a7fb9
show products in account settings
BilalG1 Dec 17, 2025
e8aa3f7
Merge remote-tracking branch 'origin/dev' into payouts-tab
BilalG1 Jan 6, 2026
3033afe
only show payments when stripe customer exists
BilalG1 Jan 6, 2026
31ccc03
Merge remote-tracking branch 'origin/payments-update-billing-info' in…
BilalG1 Jan 6, 2026
23e0ab8
small fix
BilalG1 Jan 6, 2026
a3fd753
Merge branch 'dev' into payouts-tab
BilalG1 Jan 9, 2026
5545238
empty
BilalG1 Jan 9, 2026
1bbb395
cancel subscription endpoint (#1067)
BilalG1 Jan 9, 2026
d3ac124
merge
BilalG1 Jan 9, 2026
0c63a62
Merge branch 'dev' into payouts-tab
BilalG1 Jan 9, 2026
b8c4512
Merge remote-tracking branch 'origin/payouts-tab' into payments-shown…
BilalG1 Jan 9, 2026
e1f2daf
pnpm lock
BilalG1 Jan 9, 2026
3ddf71f
Merge branch 'dev' into payments-shown-subscriptions-in-settings
BilalG1 Jan 9, 2026
1cf1779
team selector in payments panel
BilalG1 Jan 9, 2026
a6865f7
Merge branch 'dev' into payments-shown-subscriptions-in-settings
BilalG1 Jan 9, 2026
bbaf344
refactor ensureClientCanAccessCustomer
BilalG1 Jan 9, 2026
744bed1
remove
BilalG1 Jan 9, 2026
bc6fa78
hide section when no plans
BilalG1 Jan 13, 2026
868a16b
Merge remote-tracking branch 'origin/dev' into payments-shown-subscri…
BilalG1 Jan 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ensureClientCanAccessCustomer, getDefaultCardPaymentMethodSummary, getStripeCustomerForCustomerOrNull } from "@/lib/payments";
import { getStripeForAccount } from "@/lib/stripe";
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, clientOrHigherAuthTypeSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";

export const GET = createSmartRouteHandler({
metadata: {
summary: "Get payment method info",
hidden: true,
tags: ["Payments"],
},
request: yupObject({
auth: yupObject({
type: clientOrHigherAuthTypeSchema.defined(),
project: adaptSchema.defined(),
tenancy: adaptSchema.defined(),
}).defined(),
params: yupObject({
customer_type: yupString().oneOf(["user", "team"]).defined(),
customer_id: yupString().defined(),
}).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
has_customer: yupBoolean().defined(),
default_payment_method: yupObject({
id: yupString().defined(),
brand: yupString().nullable().defined(),
last4: yupString().nullable().defined(),
exp_month: yupNumber().nullable().defined(),
exp_year: yupNumber().nullable().defined(),
}).nullable().defined(),
}).defined(),
}),
handler: async ({ auth, params }, fullReq) => {
if (auth.type === "client") {
await ensureClientCanAccessCustomer({
customerType: params.customer_type,
customerId: params.customer_id,
user: fullReq.auth?.user,
tenancy: auth.tenancy,
forbiddenMessage: "Clients can only manage their own billing.",
});
}

const project = await globalPrismaClient.project.findUnique({
where: { id: auth.tenancy.project.id },
select: { stripeAccountId: true },
});
const stripeAccountId = project?.stripeAccountId;
if (!stripeAccountId) {
return {
statusCode: 200,
bodyType: "json",
body: {
has_customer: false,
default_payment_method: null,
},
};
}

const prisma = await getPrismaClientForTenancy(auth.tenancy);
const stripe = await getStripeForAccount({ accountId: stripeAccountId });
const stripeCustomer = await getStripeCustomerForCustomerOrNull({
stripe,
prisma,
tenancyId: auth.tenancy.id,
customerType: params.customer_type,
customerId: params.customer_id,
});

if (!stripeCustomer) {
return {
statusCode: 200,
bodyType: "json",
body: {
has_customer: false,
default_payment_method: null,
},
};
}

const defaultPaymentMethod = await getDefaultCardPaymentMethodSummary({
stripe,
stripeCustomer,
});

return {
statusCode: 200,
bodyType: "json",
body: {
has_customer: true,
default_payment_method: defaultPaymentMethod,
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ensureClientCanAccessCustomer, ensureStripeCustomerForCustomer, getDefaultCardPaymentMethodSummary } from "@/lib/payments";
import { getStripeForAccount } from "@/lib/stripe";
import { getPrismaClientForTenancy } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, clientOrHigherAuthTypeSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";

export const POST = createSmartRouteHandler({
metadata: {
summary: "Set default payment method from a setup intent",
hidden: true,
tags: ["Payments"],
},
request: yupObject({
auth: yupObject({
type: clientOrHigherAuthTypeSchema.defined(),
project: adaptSchema.defined(),
tenancy: adaptSchema.defined(),
}).defined(),
params: yupObject({
customer_type: yupString().oneOf(["user", "team"]).defined(),
customer_id: yupString().defined(),
}).defined(),
body: yupObject({
setup_intent_id: yupString().defined(),
}).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().oneOf([true]).defined(),
default_payment_method: yupObject({
id: yupString().defined(),
brand: yupString().nullable().defined(),
last4: yupString().nullable().defined(),
exp_month: yupNumber().nullable().defined(),
exp_year: yupNumber().nullable().defined(),
}).nullable().defined(),
}).defined(),
}),
handler: async ({ auth, params, body }, fullReq) => {
if (auth.type === "client") {
await ensureClientCanAccessCustomer({
customerType: params.customer_type,
customerId: params.customer_id,
user: fullReq.auth?.user,
tenancy: auth.tenancy,
forbiddenMessage: "Clients can only manage their own payment method.",
});
}

const prisma = await getPrismaClientForTenancy(auth.tenancy);
const stripe = await getStripeForAccount({ tenancy: auth.tenancy });
const stripeCustomer = await ensureStripeCustomerForCustomer({
stripe,
prisma,
tenancyId: auth.tenancy.id,
customerType: params.customer_type,
customerId: params.customer_id,
});

const setupIntent = await stripe.setupIntents.retrieve(body.setup_intent_id);
if (setupIntent.customer !== stripeCustomer.id) {
throw new StatusError(StatusError.Forbidden, "Setup intent does not belong to this customer.");
}
if (setupIntent.status !== "succeeded") {
throw new StatusError(400, "Setup intent has not succeeded.");
}
if (!setupIntent.payment_method || typeof setupIntent.payment_method !== "string") {
throw new StatusError(500, "Setup intent missing payment method.");
}

await stripe.customers.update(stripeCustomer.id, {
invoice_settings: {
default_payment_method: setupIntent.payment_method,
},
});

const updatedCustomer = await stripe.customers.retrieve(stripeCustomer.id);
if (updatedCustomer.deleted) {
throw new StatusError(500, "Stripe customer was deleted unexpectedly.");
}

const summary = await getDefaultCardPaymentMethodSummary({
stripe,
stripeCustomer: updatedCustomer,
});

return {
statusCode: 200,
bodyType: "json",
body: {
success: true,
default_payment_method: summary,
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ensureClientCanAccessCustomer, ensureStripeCustomerForCustomer } from "@/lib/payments";
import { getStripeForAccount } from "@/lib/stripe";
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, clientOrHigherAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";

export const POST = createSmartRouteHandler({
metadata: {
summary: "Create a setup intent to update default payment method",
hidden: true,
tags: ["Payments"],
},
request: yupObject({
auth: yupObject({
type: clientOrHigherAuthTypeSchema.defined(),
project: adaptSchema.defined(),
tenancy: adaptSchema.defined(),
}).defined(),
params: yupObject({
customer_type: yupString().oneOf(["user", "team"]).defined(),
customer_id: yupString().defined(),
}).defined(),
body: yupObject({}).default(() => ({})).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
client_secret: yupString().defined(),
stripe_account_id: yupString().defined(),
}).defined(),
}),
handler: async ({ auth, params }, fullReq) => {
if (auth.type === "client") {
await ensureClientCanAccessCustomer({
customerType: params.customer_type,
customerId: params.customer_id,
user: fullReq.auth?.user,
tenancy: auth.tenancy,
forbiddenMessage: "Clients can only manage their own payment method.",
});
}

const prisma = await getPrismaClientForTenancy(auth.tenancy);
const stripe = await getStripeForAccount({ tenancy: auth.tenancy });
const stripeCustomer = await ensureStripeCustomerForCustomer({
stripe,
prisma,
tenancyId: auth.tenancy.id,
customerType: params.customer_type,
customerId: params.customer_id,
});

const setupIntent = await stripe.setupIntents.create({
customer: stripeCustomer.id,
usage: "off_session",
payment_method_types: ["card"],
});
if (!setupIntent.client_secret) {
throw new StatusError(500, "No client secret returned from Stripe.");
}

const project = await globalPrismaClient.project.findUnique({
where: { id: auth.tenancy.project.id },
select: { stripeAccountId: true },
});
const stripeAccountId = project?.stripeAccountId;
if (!stripeAccountId) {
throw new StatusError(400, "Payments are not set up in this Stack Auth project.");
}

return {
statusCode: 200,
bodyType: "json",
body: {
client_secret: setupIntent.client_secret,
stripe_account_id: stripeAccountId,
},
};
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export const GET = createSmartRouteHandler({
id: product.id,
quantity: product.quantity,
product: productToInlineProduct(product.product),
type: product.type,
subscription: product.subscription ? {
current_period_end: product.subscription.currentPeriodEnd ? product.subscription.currentPeriodEnd.toISOString() : null,
cancel_at_period_end: product.subscription.cancelAtPeriodEnd,
is_cancelable: product.subscription.isCancelable,
} : null,
},
}));

Expand Down
Loading