我正在使用 Next.js、Supabase 和 Stripe 构建基于订阅的 SaaS 产品。
到目前为止,它正在发挥作用,但我正在努力寻找正确的付款工作流程。
我提供两种类型的订阅:每月 5 欧元,每年 50 欧元。这被建模为一个
Product
和两个 Price
。
这两个计划都可以完全访问所有应用程序功能,唯一的区别是一年后节省了多少钱。
我想为这两个计划提供 30 天的试用期。 30 天试用期结束后,订阅将
active
变成所选的两个计划之一。
我使用 webhooks 更新 Supabase 中的数据库,其中存储了
products
、prices
、customers
、subscriptions
和 users
,遵循官方模板。
我目前有两个促销代码,可以随时应用于任一计划(试用期或有效订阅期间):
当前的工作流程是这样的:
/register
页面上,用户选择其中一个计划。我将 priceId
存储在网址中,然后显示注册表单(只需电子邮件和密码)。/account/billing
页面的任何位置,用户都可以应用现有促销代码之一。他们还可以访问客户门户来添加付款详细信息。根据我对 stripe API 的了解,我似乎找不到正确的方法来处理事情。
trialing
)付款推迟到促销代码所规定的“几个月”。我没有找到方法来做到这一点,因为我无法将 current_period_end
或 billing_cycle
修改为任意值。我对这种情况最接近的是使用“订阅时间表”:我分两个阶段创建一个时间表:
trial_ends
(如果 trialing
)或 current_period_end
(如果 active
)这种方法似乎有效,但如果用户在第一阶段选择更改计划,事情就会变得混乱。有一个解决方法(拦截
subscription.update
事件并相应地更新数据),但我认为这会让事情变得过于复杂。
另一种方法是更改/延长试用期,但我更希望用户 trialing
与使用优惠券 active
订阅的用户之间有明确的界限。
export const addPromoCode = async (code: string) => {
const annualPriceId = PRICES.ANNUAL;
const monthlyPriceId = PRICES.MONTHLY;
let coupon = undefined;
try {
coupon = await getPromoDetails(code);
} catch (error: any) {
return { status: FORM_STATUS.ERROR, message: error.message };
}
const {
percent_off,
id: couponId,
duration,
duration_in_months,
} = coupon || {};
const isFreeForever = percent_off === 100 && duration === "forever";
const { id, trial_end, current_period_end, customer } =
await createOrRetrieveSubscription(
isFreeForever ? annualPriceId : monthlyPriceId
);
if (isFreeForever) {
await stripe.subscriptions.update(id, {
discounts: [{ coupon: couponId }],
trial_end: "now",
});
} else if (duration_in_months) {
await stripe.subscriptionSchedules.create({
customer: typeof customer === "string" ? customer : customer.id,
start_date: trial_end || current_period_end,
end_behavior: "release",
phases: [
{
items: [{ price: monthlyPriceId, quantity: 1 }],
iterations: duration_in_months,
billing_cycle_anchor: "phase_start",
proration_behavior: "none",
discounts: [{ coupon: couponId }],
},
{
items: [{ price: monthlyPriceId, quantity: 1 }],
},
],
});
}
revalidatePath(LINKS.BILLING);
return {
status: FORM_STATUS.SUCCESS,
message: "Promo code applied successfully!",
};
};
不幸的是,没有更优雅的解决方案。您使用订阅计划 API 构建的工作流程是处理此用例的最优雅的方式。