Menu

1.2.0

提示

版本号请查看 package.json 文件的 version 字段

定价管理支持设置优惠券

prices-coupon

实现的需求:

  • 从 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 表相关功能;如果你的产品还未线上运行或者还没有付费用户,请按照下面的步骤升级代码。

credit-history

数据库设计

  1. 删除原有的积分扣除方法:
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);
  1. 创建 credit_logs 表,作为积分变动的流水账,记录每一次变动,形成完整的审计日志;创建新的 RPC,分别用于付款记录积分、使用功能扣积分和退款回收积分

请查看源码 data/8、credit_logs.sql

  1. 更新本地 Supabase types
supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts
  1. 更新用户使用积分示例页面和方法
  • app/[locale]/(protected)/dashboard/credit-usage-example/page.tsx
  • actions/usage/deduct.ts
  1. 更新 webhook 事件的处理逻辑

这部分更新影响自定义用户权益升降级的方法,如果你已经自己实现了自定义方法,可按照相同的思路完善功能,如果未改动自定义方法,可全量覆盖 lib/stripe/webhook-handlers.ts

  1. 创建「积分历史」页面

新增文件:

  • 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 ...
  }