1.2.0
提示
版本号请查看
package.json
文件的version
字段
定价管理支持设置优惠券

实现的需求:
- 从 Stripe 拉取创建的优惠券
- 可选优惠券
- 展示的原价是 Stripe 创建的产品价格,当前价格是计算优惠券后的价格
- 如果设置了优惠券,用户点击购买按钮,会自动使用优惠券,无需用户手动输入
- 可选是否展示手动输入优惠券的购买入口
更新步骤
步骤1:在 Supabase SQL Editor 执行如下命令,添加表字段
ALTER TABLE public.pricing_plans
ADD COLUMN stripe_coupon_id character varying(255) NULL,
ADD COLUMN enable_manual_input_coupon boolean DEFAULT false NOT NULL;
COMMENT ON COLUMN public.pricing_plans.stripe_coupon_id IS 'The ID of the Stripe coupon associated with this plan.';
步骤2:执行命令更新 Supabase types
supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts
步骤3:修改 types/pricing.ts
定义
types/pricing.ts
export interface PricingPlan {
// ...other code...
stripe_coupon_id?: string | null; // add
enable_manual_input_coupon?: boolean; // add
}
步骤4:修改 actions/prices/admin.ts
创建定价计划的方法
actions/prices/admin.ts
export async function createPricingPlanAction({
planData,
locale = DEFAULT_LOCALE,
}: CreatePricingPlanParams) {
// ...other code...
const { data, error } = await supabaseAdmin
.from("pricing_plans")
.insert({
// ...other code...
stripe_coupon_id: planData.stripe_coupon_id,
enable_manual_input_coupon: planData.enable_manual_input_coupon ?? false,
})
// ...other code...
}
步骤5:修改 /prices/PricePlanForm.tsx
,扩展新增需求
app/[locale]/(protected)/dashboard/(admin)/prices/PricePlanForm.tsx
// 1.扩展表格类型定义
const pricingPlanFormSchema = z.object({
// ...other code...
stripe_coupon_id: z.string().optional().nullable(),
enable_manual_input_coupon: z.boolean().optional().nullable(),
})
// 2.提供优惠码功能相关状态
const [isFetchingCoupons, setIsFetchingCoupons] = useState(false);
const [coupons, setCoupons] = useState<any[]>([]);
// 3.扩展表格定义
const form = useForm<PricingPlanFormValues>({
resolver: zodResolver(pricingPlanFormSchema),
defaultValues: {
// ... other code ...
stripe_coupon_id: initialData?.stripe_coupon_id ?? "",
enable_manual_input_coupon: initialData?.enable_manual_input_coupon ?? false,
}
})
// 4.监听 stripe_coupon_id 变化,用于计算展示价格
const watchStripeCouponId = form.watch("stripe_coupon_id");
// 5.添加优惠码相关方法
useEffect(() => {
// 请直接查看源码,复制完整方法
}, [watchEnvironment]);
useEffect(() => {
// 请直接查看源码,复制完整方法
}, [watchStripeCouponId, coupons]);
const handleFetchCoupons = async () => {
// 请直接查看源码,复制完整方法
};
// 6.优化展示价格和原价的自定义展示逻辑
const handleStripeVerify = async () => {
// ... other code ...
// remove
// if (!form.getValues("display_price")) {
// const formattedPrice = await formatCurrency(
// priceInCorrectUnit,
// currency
// );
// form.setValue("display_price", formattedPrice, {
// shouldValidate: true,
// });
// }
// add
const formattedPrice = await formatCurrency(priceInCorrectUnit, currency);
form.setValue("original_price", formattedPrice, {
shouldValidate: true,
});
if (!form.getValues("display_price")) {
form.setValue("display_price", formattedPrice, {
shouldValidate: true,
});
}
// ... other code ...
}
// 7.在 Stripe 集成板块里添加表单项
<FormField
control={form.control}
name="stripe_coupon_id"
render={({ field }) => (
/* 请直接查看源码,复制完整组件 */
)}
/>
{watchStripeCouponId && (
<FormField
control={form.control}
name="enable_manual_input_coupon"
render={({ field }) => (
/* 请直接查看源码,复制完整组件 */
)}
/>
)}
步骤6:添加一个新的接口获取优惠码
app/api/admin/stripe/coupons/route.ts
// 请直接查看源码,复制完整代码
步骤7:修改购买按钮,集成自动传优惠码的功能
components/home/PricingCTA.tsx
// 1.修改 handleCheckout,支持优惠码功能
const handleCheckout = async (applyCoupon = true) => {
const stripePriceId = plan.stripe_price_id ?? null;
if (!stripePriceId) {
toast.error("Price ID is missing for this plan.");
return;
}
const couponCode = plan.stripe_coupon_id;
try {
const toltReferral = (window as any).tolt_referral;
const requestBody: {
priceId: string;
couponCode?: string;
referral?: string;
} = {
priceId: stripePriceId,
};
if (applyCoupon && couponCode) {
requestBody.couponCode = couponCode;
}
if (toltReferral) {
requestBody.referral = toltReferral;
}
const response = await fetch("/api/stripe/checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept-Language": (locale || DEFAULT_LOCALE) as string,
},
body: JSON.stringify(requestBody),
});
// ... other code ...
} catch (error) {
// ... other code ...
}
}
// 2.修改按钮,支持优惠码功能
return (
<div>
<Button
asChild={!!plan.button_link}
disabled={isLoading}
// 修改 className
className={`w-full flex items-center justify-center gap-2 text-white py-5 font-medium ${
plan.is_highlighted ? highlightedCtaStyle : defaultCtaStyle
} ${
plan.stripe_coupon_id && plan.enable_manual_input_coupon
? "mb-2"
: "mb-6"
}`}
{...(!plan.button_link && {
onClick: () => handleCheckout(),
})}
>
</Button>
/* 添加新购买入口 */
{plan.stripe_coupon_id && plan.enable_manual_input_coupon && (
<div className="text-center mb-2">
<button
onClick={() => handleCheckout(false)}
disabled={isLoading}
className="text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50 underline underline-offset-2"
>
I have a different coupon code
</button>
</div>
)}
</div>
);
新增 credit_logs
表
该变动会影响原有的用户权益升降级处理方法,如果你的代码早于 1.2.0 且已经有付费用户,应该自己实现 credit_logs
表相关功能;如果你的产品还未线上运行或者还没有付费用户,请按照下面的步骤升级代码。

数据库设计
- 删除原有的积分扣除方法:
DROP FUNCTION IF EXISTS public.upsert_and_increment_one_time_credits;
DROP FUNCTION IF EXISTS public.upsert_and_set_subscription_credits;
DROP FUNCTION IF EXISTS public.revoke_credits;
DROP FUNCTION IF EXISTS public.deduct_one_time_credits(uuid, integer);
DROP FUNCTION IF EXISTS public.deduct_subscription_credits(uuid, integer);
DROP FUNCTION IF EXISTS public.deduct_credits_priority_subscription(uuid, integer);
DROP FUNCTION IF EXISTS public.deduct_credits_priority_one_time(uuid, integer);
- 创建
credit_logs
表,作为积分变动的流水账,记录每一次变动,形成完整的审计日志;创建新的 RPC,分别用于付款记录积分、使用功能扣积分和退款回收积分
请查看源码 data/8、credit_logs.sql
- 更新本地 Supabase types
supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts
- 更新用户使用积分示例页面和方法
app/[locale]/(protected)/dashboard/credit-usage-example/page.tsx
actions/usage/deduct.ts
- 更新 webhook 事件的处理逻辑
这部分更新影响自定义用户权益升降级的方法,如果你已经自己实现了自定义方法,可按照相同的思路完善功能,如果未改动自定义方法,可全量覆盖 lib/stripe/webhook-handlers.ts
。
- 创建「积分历史」页面
新增文件:
actions/usage/logs.ts
app/[locale]/(protected)/dashboard/(user)/credit-history/*
i18n/en/CreditHistory.json
i18n/zh/CreditHistory.json
i18n/ja/CreditHistory.json
修改文件:
i18n/request.ts
messages: {
CreditHistory: (await import(`./messages/${locale}/CreditHistory.json`)).default,
// ... other code ...
}