Menu

1.2.0

Good to know

Please check the version field in the package.json file for the version number

Pricing Management Supports Coupon Configuration

prices-coupon

Implemented features:

  • Pull created coupons from Stripe
  • Selectable coupons
  • Display original price as the product price created in Stripe, current price is calculated after applying the coupon
  • If a coupon is set, when users click the purchase button, the coupon will be automatically applied without manual input
  • Option to display manual coupon input purchase entry

Update Steps

Step 1: Execute the following command in Supabase SQL Editor to add table fields

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.'; 

Step 2: Execute command to update Supabase types

supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts

Step 3: Modify types/pricing.ts definition

types/pricing.ts
export interface PricingPlan {
  // ...other code...
  stripe_coupon_id?: string | null; // add
  enable_manual_input_coupon?: boolean; // add
}

Step 4: Modify the create pricing plan method in 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...
}

Step 5: Modify /prices/PricePlanForm.tsx to extend new requirements

app/[locale]/(protected)/dashboard/(admin)/prices/PricePlanForm.tsx
// 1. Extend form type definition
const pricingPlanFormSchema = z.object({
  // ...other code...
  stripe_coupon_id: z.string().optional().nullable(),
  enable_manual_input_coupon: z.boolean().optional().nullable(),
})
 
// 2. Provide coupon functionality related state
  const [isFetchingCoupons, setIsFetchingCoupons] = useState(false);
  const [coupons, setCoupons] = useState<any[]>([]);
 
// 3. Extend form definition
  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. Watch stripe_coupon_id changes for calculating display price
  const watchStripeCouponId = form.watch("stripe_coupon_id");
 
// 5. Add coupon related methods
  useEffect(() => {
    // Please check the source code directly and copy the complete method
  }, [watchEnvironment]);
 
  useEffect(() => {
    // Please check the source code directly and copy the complete method
  }, [watchStripeCouponId, coupons]);
 
  const handleFetchCoupons = async () => {
    // Please check the source code directly and copy the complete method
  };
 
// 6. Optimize custom display logic for display price and original price
 
  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. Add form items in the Stripe integration section
  <FormField
    control={form.control}
    name="stripe_coupon_id"
    render={({ field }) => (
      /* Please check the source code directly and copy the complete component */
    )}
  />
  
  {watchStripeCouponId && (
    <FormField
      control={form.control}
      name="enable_manual_input_coupon"
      render={({ field }) => (
        /* Please check the source code directly and copy the complete component */
      )}
    />
  )}

Step 6: Add a new API to get coupons

app/api/admin/stripe/coupons/route.ts
// Please check the source code directly and copy the complete code

Step 7: Modify the purchase button to integrate automatic coupon passing functionality

components/home/PricingCTA.tsx
  // 1. Modify handleCheckout to support coupon functionality
  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. Modify button to support coupon functionality
  return (
    <div>
      <Button
        asChild={!!plan.button_link}
        disabled={isLoading}
        // Modify 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>
      /* Add new purchase entry */
      {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>
  );

New credit_logs Table

This change will affect the existing user privilege upgrade/downgrade handling methods. If your code is earlier than 1.2.0 and you already have paying users, you should implement the credit_logs table related functionality yourself. If your product is not yet live or you don't have paying users yet, please follow the steps below to upgrade the code.

credit-history

Database Design

  1. Delete the original credit deduction methods:
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. Create the credit_logs table as a transaction log for credit changes, recording every change to form a complete audit trail. Create new RPCs for recording credits on payment, deducting credits for feature usage, and recovering credits on refund.

Please check the source code data/8、credit_logs.sql

  1. Update local Supabase types
supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts
  1. Update user credit usage example page and methods
  • app/[locale]/(protected)/dashboard/credit-usage-example/page.tsx
  • actions/usage/deduct.ts
  1. Update webhook event handling logic

This update affects custom user privilege upgrade/downgrade methods. If you have already implemented custom methods, you can follow the same approach to improve functionality. If you haven't modified custom methods, you can fully overwrite lib/stripe/webhook-handlers.ts.

  1. Create "Credit History" page

New files:

  • actions/usage/logs.ts
  • app/[locale]/(protected)/dashboard/(user)/credit-history/*
  • i18n/en/CreditHistory.json
  • i18n/zh/CreditHistory.json
  • i18n/ja/CreditHistory.json

Modified files:

  • i18n/request.ts
  messages: {
    CreditHistory: (await import(`./messages/${locale}/CreditHistory.json`)).default,
    // ... other code ...
  }