feat: add pricing plans and enhance payment configuration
- Introduced new pricing plans (Free, Pro, Lifetime) with detailed descriptions and features in both English and Chinese. - Updated payment configuration to include new plans and their pricing structures. - Refactored payment-related functions to retrieve pricing plans consistently across the application. - Enhanced the PricingTable component to dynamically display available plans based on the new configuration. - Removed deprecated payment configuration file to streamline the codebase.
This commit is contained in:
parent
31a4823b54
commit
165673a998
@ -512,6 +512,43 @@
|
||||
"button": "Upgrade"
|
||||
}
|
||||
},
|
||||
"PricePlans": {
|
||||
"free": {
|
||||
"name": "Free",
|
||||
"description": "Basic features for personal use",
|
||||
"features": {
|
||||
"projects": "Up to 3 projects",
|
||||
"storage": "1 GB storage",
|
||||
"analytics": "Basic analytics",
|
||||
"support": "Community support"
|
||||
}
|
||||
},
|
||||
"pro": {
|
||||
"name": "Pro",
|
||||
"description": "Advanced features for professionals",
|
||||
"features": {
|
||||
"projects": "Unlimited projects",
|
||||
"storage": "10 GB storage",
|
||||
"analytics": "Advanced analytics",
|
||||
"support": "Priority support",
|
||||
"domains": "Custom domains",
|
||||
"collaboration": "Team collaboration"
|
||||
}
|
||||
},
|
||||
"lifetime": {
|
||||
"name": "Lifetime",
|
||||
"description": "Premium features with one-time payment",
|
||||
"features": {
|
||||
"proFeatures": "All Pro features",
|
||||
"storage": "100 GB storage",
|
||||
"support": "Dedicated support",
|
||||
"security": "Enterprise-grade security",
|
||||
"integrations": "Advanced integrations",
|
||||
"branding": "Custom branding",
|
||||
"updates": "Lifetime updates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mail": {
|
||||
"common": {
|
||||
"team": "{name} Team",
|
||||
|
@ -511,6 +511,43 @@
|
||||
"button": "升级"
|
||||
}
|
||||
},
|
||||
"PricePlans": {
|
||||
"free": {
|
||||
"name": "免费版",
|
||||
"description": "适用于个人使用的基本功能",
|
||||
"features": {
|
||||
"projects": "最多3个项目",
|
||||
"storage": "1GB存储空间",
|
||||
"analytics": "基础分析功能",
|
||||
"support": "社区支持"
|
||||
}
|
||||
},
|
||||
"pro": {
|
||||
"name": "专业版",
|
||||
"description": "专业人士的高级功能",
|
||||
"features": {
|
||||
"projects": "无限项目",
|
||||
"storage": "10GB存储空间",
|
||||
"analytics": "高级分析功能",
|
||||
"support": "优先支持",
|
||||
"domains": "自定义域名",
|
||||
"collaboration": "团队协作"
|
||||
}
|
||||
},
|
||||
"lifetime": {
|
||||
"name": "终身版",
|
||||
"description": "一次性付款获得所有高级功能",
|
||||
"features": {
|
||||
"proFeatures": "所有专业版功能",
|
||||
"storage": "100GB存储空间",
|
||||
"support": "专属支持",
|
||||
"security": "企业级安全",
|
||||
"integrations": "高级集成",
|
||||
"branding": "自定义品牌",
|
||||
"updates": "终身更新"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mail": {
|
||||
"common": {
|
||||
"team": "{name} 团队",
|
||||
|
@ -3,7 +3,7 @@
|
||||
import db from "@/db";
|
||||
import { payment } from "@/db/schema";
|
||||
import { getSession } from "@/lib/server";
|
||||
import { getAllPlans } from "@/payment";
|
||||
import { getAllPricePlans } from "@/payment";
|
||||
import { PaymentTypes } from "@/payment/types";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
@ -51,7 +51,7 @@ export const getLifetimeStatusAction = actionClient
|
||||
|
||||
try {
|
||||
// Get lifetime plans
|
||||
const plans = getAllPlans();
|
||||
const plans = getAllPricePlans();
|
||||
const lifetimePlanIds = plans
|
||||
.filter(plan => plan.isLifetime)
|
||||
.map(plan => plan.id);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HeaderSection } from '@/components/layout/header-section';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getBaseUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { getAllPlans } from '@/payment';
|
||||
import { getAllPricePlans } from '@/payment';
|
||||
import { Metadata } from 'next';
|
||||
import { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
@ -29,7 +29,7 @@ export default async function PricingPageLayout({
|
||||
const t = await getTranslations('PricingPage');
|
||||
|
||||
// Get all plans as an array
|
||||
const plans = getAllPlans();
|
||||
const plans = getAllPricePlans();
|
||||
|
||||
return (
|
||||
<div className="mb-16">
|
||||
|
@ -1,14 +1,10 @@
|
||||
import Container from '@/components/layout/container';
|
||||
import { PricingTable } from '@/components/payment/pricing-table';
|
||||
import { getAllPlans } from '@/payment';
|
||||
|
||||
export default async function PricingPage() {
|
||||
// Get all plans as an array
|
||||
const plans = getAllPlans();
|
||||
|
||||
return (
|
||||
<Container className="mt-8 px-4 max-w-6xl">
|
||||
<PricingTable plans={plans} />
|
||||
<PricingTable />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ export function PricingCard({
|
||||
|
||||
{/* show features of this plan */}
|
||||
<ul className="list-outside space-y-4 text-sm">
|
||||
{plan.features.map((feature, i) => (
|
||||
{plan.features?.map((feature, i) => (
|
||||
<li key={i} className="flex items-center gap-2">
|
||||
<CheckCircleIcon className="size-4 text-green-500 dark:text-green-400" />
|
||||
<span>{feature}</span>
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { getPricePlanInfos } from '@/config';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PaymentTypes, PlanInterval, PlanIntervals, PricePlan } from '@/payment/types';
|
||||
import { useTranslations } from 'next-intl';
|
||||
@ -8,7 +9,6 @@ import { useState } from 'react';
|
||||
import { PricingCard } from './pricing-card';
|
||||
|
||||
interface PricingTableProps {
|
||||
plans: PricePlan[];
|
||||
metadata?: Record<string, string>;
|
||||
currentPlan?: PricePlan | null;
|
||||
className?: string;
|
||||
@ -23,7 +23,6 @@ interface PricingTableProps {
|
||||
* 3. If a price is disabled, it will not be displayed in the pricing table
|
||||
*/
|
||||
export function PricingTable({
|
||||
plans,
|
||||
metadata,
|
||||
currentPlan,
|
||||
className,
|
||||
@ -31,6 +30,10 @@ export function PricingTable({
|
||||
const t = useTranslations('PricingPage');
|
||||
const [interval, setInterval] = useState<PlanInterval>(PlanIntervals.MONTH);
|
||||
|
||||
// Get plans either from props or from the config
|
||||
const paymentConfig = getPricePlanInfos();
|
||||
const plans = Object.values(paymentConfig.plans);
|
||||
|
||||
// Current plan ID for comparison
|
||||
const currentPlanId = currentPlan?.id || null;
|
||||
|
||||
|
108
src/config.tsx
108
src/config.tsx
@ -5,6 +5,11 @@ import { InstagramIcon } from '@/components/icons/instagram';
|
||||
import { LinkedInIcon } from '@/components/icons/linkedin';
|
||||
import { TikTokIcon } from '@/components/icons/tiktok';
|
||||
import { YouTubeIcon } from '@/components/icons/youtube';
|
||||
import {
|
||||
PaymentConfig,
|
||||
PaymentTypes,
|
||||
PlanIntervals
|
||||
} from '@/payment/types';
|
||||
import { Routes } from '@/routes';
|
||||
import { MenuItem, NestedMenuItem, WebsiteConfig } from '@/types';
|
||||
import {
|
||||
@ -73,6 +78,51 @@ export const websiteConfig: WebsiteConfig = {
|
||||
facebook: 'https://facebook.com/mksaas',
|
||||
instagram: 'https://instagram.com/mksaas',
|
||||
tiktok: 'https://tiktok.com/@mksaas',
|
||||
},
|
||||
payment: {
|
||||
plans: {
|
||||
free: {
|
||||
id: "free",
|
||||
prices: [],
|
||||
isFree: true,
|
||||
isLifetime: false,
|
||||
},
|
||||
pro: {
|
||||
id: "pro",
|
||||
prices: [
|
||||
{
|
||||
type: PaymentTypes.SUBSCRIPTION,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY!,
|
||||
amount: 990,
|
||||
currency: "USD",
|
||||
interval: PlanIntervals.MONTH,
|
||||
},
|
||||
{
|
||||
type: PaymentTypes.SUBSCRIPTION,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO_YEARLY!,
|
||||
amount: 9900,
|
||||
currency: "USD",
|
||||
interval: PlanIntervals.YEAR,
|
||||
},
|
||||
],
|
||||
isFree: false,
|
||||
isLifetime: false,
|
||||
recommended: true,
|
||||
},
|
||||
lifetime: {
|
||||
id: "lifetime",
|
||||
prices: [
|
||||
{
|
||||
type: PaymentTypes.ONE_TIME,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_LIFETIME!,
|
||||
amount: 19900,
|
||||
currency: "USD",
|
||||
},
|
||||
],
|
||||
isFree: false,
|
||||
isLifetime: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -563,3 +613,61 @@ export function getSocialLinks(): MenuItem[] {
|
||||
|
||||
return socialLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price plans with translations for client components
|
||||
*
|
||||
* NOTICE: This function should only be used in client components
|
||||
*
|
||||
* @returns The price plans with translated content
|
||||
*/
|
||||
export function getPricePlanInfos(): PaymentConfig {
|
||||
const t = useTranslations('PricePlans');
|
||||
const { payment } = websiteConfig;
|
||||
|
||||
// Create a deep clone of the payment config to avoid modifying the original
|
||||
const paymentConfig: PaymentConfig = {
|
||||
plans: JSON.parse(JSON.stringify(payment.plans))
|
||||
};
|
||||
|
||||
// Add translated content to each plan
|
||||
if (paymentConfig.plans.free) {
|
||||
paymentConfig.plans.free.name = t('free.name');
|
||||
paymentConfig.plans.free.description = t('free.description');
|
||||
paymentConfig.plans.free.features = [
|
||||
t('free.features.projects'),
|
||||
t('free.features.analytics'),
|
||||
t('free.features.support'),
|
||||
t('free.features.storage')
|
||||
];
|
||||
}
|
||||
|
||||
if (paymentConfig.plans.pro) {
|
||||
paymentConfig.plans.pro.name = t('pro.name');
|
||||
paymentConfig.plans.pro.description = t('pro.description');
|
||||
paymentConfig.plans.pro.features = [
|
||||
t('pro.features.projects'),
|
||||
t('pro.features.analytics'),
|
||||
t('pro.features.support'),
|
||||
t('pro.features.storage'),
|
||||
t('pro.features.domains'),
|
||||
t('pro.features.collaboration')
|
||||
];
|
||||
}
|
||||
|
||||
if (paymentConfig.plans.lifetime) {
|
||||
paymentConfig.plans.lifetime.name = t('lifetime.name');
|
||||
paymentConfig.plans.lifetime.description = t('lifetime.description');
|
||||
paymentConfig.plans.lifetime.features = [
|
||||
t('lifetime.features.proFeatures'),
|
||||
t('lifetime.features.security'),
|
||||
t('lifetime.features.support'),
|
||||
t('lifetime.features.storage'),
|
||||
t('lifetime.features.integrations'),
|
||||
t('lifetime.features.branding'),
|
||||
t('lifetime.features.updates')
|
||||
];
|
||||
}
|
||||
|
||||
return paymentConfig;
|
||||
}
|
||||
|
@ -1,98 +0,0 @@
|
||||
import { PaymentConfig, PaymentTypes, PlanIntervals, PricePlan } from "../types";
|
||||
|
||||
/**
|
||||
* Free plan definition
|
||||
*/
|
||||
const freePlan: PricePlan = {
|
||||
id: "free",
|
||||
name: "Free",
|
||||
description: "Basic features for personal use",
|
||||
features: [
|
||||
"Up to 3 projects",
|
||||
"Basic analytics",
|
||||
"Community support",
|
||||
"1 GB storage"
|
||||
],
|
||||
prices: [],
|
||||
isFree: true,
|
||||
isLifetime: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Pro plan definition
|
||||
*/
|
||||
const proPlan: PricePlan = {
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
description: "Advanced features for professionals",
|
||||
features: [
|
||||
"Unlimited projects",
|
||||
"Advanced analytics",
|
||||
"Priority support",
|
||||
"10 GB storage",
|
||||
"Custom domains",
|
||||
"Team collaboration"
|
||||
],
|
||||
prices: [
|
||||
{
|
||||
type: PaymentTypes.SUBSCRIPTION,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY!,
|
||||
amount: 990,
|
||||
currency: "USD",
|
||||
interval: PlanIntervals.MONTH,
|
||||
},
|
||||
{
|
||||
type: PaymentTypes.SUBSCRIPTION,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO_YEARLY!,
|
||||
amount: 9900,
|
||||
currency: "USD",
|
||||
interval: PlanIntervals.YEAR,
|
||||
},
|
||||
],
|
||||
isFree: false,
|
||||
isLifetime: false,
|
||||
recommended: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Lifetime plan definition
|
||||
*/
|
||||
const lifetimePlan: PricePlan = {
|
||||
id: "lifetime",
|
||||
name: "Lifetime",
|
||||
description: "Premium features with one-time payment",
|
||||
features: [
|
||||
"All Pro features",
|
||||
"Enterprise-grade security",
|
||||
"Dedicated support",
|
||||
"100 GB storage",
|
||||
"Advanced integrations",
|
||||
"Custom branding",
|
||||
"Lifetime updates"
|
||||
],
|
||||
prices: [
|
||||
{
|
||||
type: PaymentTypes.ONE_TIME,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_LIFETIME!,
|
||||
amount: 19900,
|
||||
currency: "USD",
|
||||
},
|
||||
],
|
||||
isFree: false,
|
||||
isLifetime: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Payment configuration
|
||||
*
|
||||
* NOTICE:
|
||||
* 1. One priceId can only be used by one plan.
|
||||
*/
|
||||
export const paymentConfig: PaymentConfig = {
|
||||
plans: {
|
||||
free: freePlan,
|
||||
pro: proPlan,
|
||||
lifetime: lifetimePlan,
|
||||
},
|
||||
defaultCurrency: "USD",
|
||||
};
|
@ -1,11 +1,6 @@
|
||||
import { PaymentProvider, PricePlan, PaymentConfig, Customer, Subscription, Payment, PaymentStatus, PlanInterval, PaymentType, Price, CreateCheckoutParams, CheckoutResult, CreatePortalParams, PortalResult, getSubscriptionsParams } from "./types";
|
||||
import { StripeProvider } from "./provider/stripe";
|
||||
import { paymentConfig } from "./config/payment-config";
|
||||
|
||||
/**
|
||||
* Default payment configuration
|
||||
*/
|
||||
export const defaultPaymentConfig: PaymentConfig = paymentConfig;
|
||||
import { websiteConfig } from "@/config";
|
||||
|
||||
/**
|
||||
* Global payment provider instance
|
||||
@ -90,15 +85,15 @@ export const getSubscriptions = async (
|
||||
* @returns Plan or undefined if not found
|
||||
*/
|
||||
export const getPlanById = (planId: string): PricePlan | undefined => {
|
||||
return defaultPaymentConfig.plans[planId];
|
||||
return websiteConfig.payment.plans[planId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all available plans
|
||||
* Get all price plans
|
||||
* @returns Array of price plans
|
||||
*/
|
||||
export const getAllPlans = (): PricePlan[] => {
|
||||
return Object.values(defaultPaymentConfig.plans);
|
||||
export const getAllPricePlans = (): PricePlan[] => {
|
||||
return Object.values(websiteConfig.payment.plans);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -122,7 +117,7 @@ export const findPriceInPlan = (planId: string, priceId: string): Price | undefi
|
||||
* @returns Plan or undefined if not found
|
||||
*/
|
||||
export const findPlanByPriceId = (priceId: string): PricePlan | undefined => {
|
||||
const plans = getAllPlans();
|
||||
const plans = getAllPricePlans();
|
||||
for (const plan of plans) {
|
||||
const matchingPrice = plan.prices.find(price => price.priceId === priceId);
|
||||
if (matchingPrice) {
|
||||
|
@ -61,9 +61,9 @@ export interface Price {
|
||||
*/
|
||||
export interface PricePlan {
|
||||
id: string; // Unique identifier for the plan
|
||||
name: string; // Display name of the plan
|
||||
description: string; // Description of the plan features
|
||||
features: string[]; // List of features included in this plan
|
||||
name?: string; // Display name of the plan
|
||||
description?: string; // Description of the plan features
|
||||
features?: string[]; // List of features included in this plan
|
||||
prices: Price[]; // Available prices for this plan
|
||||
isFree: boolean; // Whether this is a free plan
|
||||
isLifetime: boolean; // Whether this is a lifetime plan
|
||||
@ -76,7 +76,6 @@ export interface PricePlan {
|
||||
*/
|
||||
export interface PaymentConfig {
|
||||
plans: Record<string, PricePlan>; // Plans indexed by ID
|
||||
defaultCurrency: string; // Default currency
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,8 +67,6 @@ STORAGE_REGION=us-east-1
|
||||
STORAGE_ACCESS_KEY_ID=your-access-key
|
||||
STORAGE_SECRET_ACCESS_KEY=your-secret-key
|
||||
STORAGE_BUCKET_NAME=your-bucket-name
|
||||
|
||||
# Optional
|
||||
STORAGE_ENDPOINT=https://custom-endpoint.com
|
||||
STORAGE_PUBLIC_URL=https://cdn.example.com
|
||||
STORAGE_FORCE_PATH_STYLE=true
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getActiveSubscriptionAction } from '@/actions/get-active-subscription';
|
||||
import { getLifetimeStatusAction } from '@/actions/get-lifetime-status';
|
||||
import { Session } from '@/lib/auth';
|
||||
import { getAllPlans } from '@/payment';
|
||||
import { getAllPricePlans } from '@/payment';
|
||||
import { PricePlan, Subscription } from '@/payment/types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
@ -55,8 +55,9 @@ export const usePaymentStore = create<PaymentState>((set, get) => ({
|
||||
// Fetch subscription data
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
// Get all available plans
|
||||
const plans = getAllPlans();
|
||||
// Get all price plans
|
||||
let plans: PricePlan[] = getAllPricePlans();
|
||||
|
||||
const freePlan = plans.find(plan => plan.isFree);
|
||||
const lifetimePlan = plans.find(plan => plan.isLifetime);
|
||||
|
||||
|
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { PaymentConfig } from '@/payment/types';
|
||||
|
||||
/**
|
||||
* website config, without translations
|
||||
@ -30,6 +31,7 @@ export type WebsiteConfig = {
|
||||
instagram?: string;
|
||||
tiktok?: string;
|
||||
};
|
||||
payment: PaymentConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user