1.2.0
Good to know
Please check the
version
field in thepackage.json
file for the version number
Pricing Management Supports Coupon Configuration

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
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
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
// 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
// Please check the source code directly and copy the complete code
Step 7: Modify the purchase button to integrate automatic coupon passing functionality
// 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.

Database Design
- 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);
- 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
- Update local Supabase types
supabase gen types typescript --project-id <your-project-id> --schema public > lib/supabase/types.ts
- Update user credit usage example page and methods
app/[locale]/(protected)/dashboard/credit-usage-example/page.tsx
actions/usage/deduct.ts
- 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
.
- 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 ...
}