@windrun-huaiin/third-ui 7.3.3 → 7.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/clerk/fingerprint/fingerprint-provider.js +26 -6
  2. package/dist/clerk/fingerprint/fingerprint-provider.mjs +27 -7
  3. package/dist/clerk/fingerprint/use-fingerprint.js +1 -1
  4. package/dist/clerk/fingerprint/use-fingerprint.mjs +1 -1
  5. package/dist/main/faq.js +0 -2
  6. package/dist/main/faq.mjs +0 -2
  7. package/dist/main/gallery.js +0 -2
  8. package/dist/main/gallery.mjs +0 -2
  9. package/dist/main/money-price/money-price-config-util.d.ts +7 -0
  10. package/dist/main/money-price/money-price-config-util.js +19 -0
  11. package/dist/main/money-price/money-price-config-util.mjs +16 -0
  12. package/dist/main/money-price/money-price-interactive.js +29 -50
  13. package/dist/main/money-price/money-price-interactive.mjs +28 -49
  14. package/dist/main/money-price/money-price-types.d.ts +1 -1
  15. package/dist/main/money-price/money-price.js +3 -23
  16. package/dist/main/money-price/money-price.mjs +2 -22
  17. package/dist/main/price-plan.js +0 -2
  18. package/dist/main/price-plan.mjs +0 -2
  19. package/dist/main/server.d.ts +1 -1
  20. package/dist/main/server.js +3 -4
  21. package/dist/main/server.mjs +1 -1
  22. package/package.json +1 -1
  23. package/src/clerk/fingerprint/fingerprint-provider.tsx +53 -20
  24. package/src/clerk/fingerprint/use-fingerprint.ts +1 -1
  25. package/src/main/money-price/money-price-config-util.ts +23 -0
  26. package/src/main/money-price/money-price-interactive.tsx +47 -64
  27. package/src/main/money-price/money-price-types.ts +1 -1
  28. package/src/main/money-price/money-price.tsx +5 -32
  29. package/src/main/server.ts +1 -2
  30. package/src/main/money-price/money-price-config.ts +0 -229
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Money Price Configuration
3
+ * 价格组件配置文件
4
+ */
5
+
6
+ import type { MoneyPriceConfig, PaymentProviderConfig, EnhancePricePlan } from './money-price-types';
7
+
8
+ // 辅助函数:获取当前激活的支付供应商配置
9
+ export function getActiveProviderConfig(config: MoneyPriceConfig): PaymentProviderConfig {
10
+ const provider = config.activeProvider;
11
+ return config.paymentProviders[provider];
12
+ }
13
+
14
+ // 辅助函数:获取特定产品的价格信息
15
+ export function getProductPricing(
16
+ productKey: 'free' | 'pro' | 'ultra',
17
+ billingType: 'monthly' | 'yearly',
18
+ provider: string,
19
+ config: MoneyPriceConfig
20
+ ): EnhancePricePlan {
21
+ const providerConfig = config.paymentProviders[provider];
22
+ return providerConfig.products[productKey].plans[billingType];
23
+ }
@@ -4,10 +4,9 @@ import { useClerk } from '@clerk/nextjs';
4
4
  import { useFingerprintContextSafe } from '@third-ui/clerk/fingerprint';
5
5
  import { cn } from '@windrun-huaiin/lib/utils';
6
6
  import { useRouter } from 'next/navigation';
7
- import React, { useEffect, useState } from 'react';
8
- import { createRoot } from 'react-dom/client';
7
+ import React, { useEffect, useState, useMemo, useCallback } from 'react';
9
8
  import { MoneyPriceButton } from './money-price-button';
10
- import { getActiveProviderConfig, getProductPricing } from './money-price-config';
9
+ import { getActiveProviderConfig, getProductPricing } from './money-price-config-util';
11
10
  import {
12
11
  UserState,
13
12
  type MoneyPriceInteractiveProps,
@@ -33,9 +32,9 @@ export function MoneyPriceInteractive({
33
32
  x: number;
34
33
  y: number;
35
34
  }>({ show: false, content: '', x: 0, y: 0 });
36
-
35
+
37
36
  // 确定用户状态
38
- const getUserState = (): UserState => {
37
+ const getUserState = useCallback((): UserState => {
39
38
  if (!fingerprintContext) return UserState.Anonymous;
40
39
  const { xUser, xSubscription } = fingerprintContext;
41
40
 
@@ -44,27 +43,27 @@ export function MoneyPriceInteractive({
44
43
  if (xSubscription.priceName?.includes('Pro')) return UserState.ProUser;
45
44
  if (xSubscription.priceName?.includes('Ultra')) return UserState.UltraUser;
46
45
  return UserState.FreeUser;
47
- };
48
-
49
- const userContext: UserContext = {
46
+ }, [fingerprintContext]);
47
+
48
+ // 优化 userContext 使用 useMemo
49
+ const userContext = useMemo<UserContext>(() => ({
50
50
  isAuthenticated: !!fingerprintContext?.xUser?.clerkUserId,
51
51
  subscriptionStatus: getUserState(),
52
52
  subscriptionType: fingerprintContext?.xSubscription?.priceId?.includes('yearly') ? 'yearly' : 'monthly',
53
53
  subscriptionEndDate: fingerprintContext?.xSubscription?.subPeriodEnd
54
- };
55
-
54
+ }), [fingerprintContext, getUserState]);
55
+
56
56
  // 处理登录
57
- const handleLogin = () => {
57
+ const handleLogin = useCallback(() => {
58
58
  if (signInPath) {
59
59
  router.push(signInPath);
60
60
  } else {
61
61
  redirectToSignIn();
62
62
  }
63
- };
64
-
63
+ }, [signInPath, redirectToSignIn, router]);
64
+
65
65
  // 处理升级
66
- const handleUpgrade = async (plan: string, billingType: string) => {
67
- // 如果没有配置 API 端点,跳转到首页
66
+ const handleUpgrade = useCallback(async (plan: string, billingType: string) => {
68
67
  if (!upgradeApiEndpoint) {
69
68
  router.push('/');
70
69
  return;
@@ -72,7 +71,6 @@ export function MoneyPriceInteractive({
72
71
 
73
72
  setIsProcessing(true);
74
73
  try {
75
- // 获取价格配置
76
74
  const pricing = getProductPricing(
77
75
  plan as 'free' | 'pro' | 'ultra',
78
76
  billingType as 'monthly' | 'yearly',
@@ -80,7 +78,6 @@ export function MoneyPriceInteractive({
80
78
  config
81
79
  );
82
80
 
83
- // 调用 API 创建支付会话
84
81
  const response = await fetch(upgradeApiEndpoint, {
85
82
  method: 'POST',
86
83
  headers: {
@@ -97,7 +94,6 @@ export function MoneyPriceInteractive({
97
94
  const result = await response.json();
98
95
 
99
96
  if (result.success && result.checkoutUrl) {
100
- // 跳转到支付页面
101
97
  window.location.href = result.checkoutUrl;
102
98
  } else {
103
99
  console.error('Failed to create checkout session:', result.error);
@@ -107,17 +103,16 @@ export function MoneyPriceInteractive({
107
103
  } finally {
108
104
  setIsProcessing(false);
109
105
  }
110
- };
111
-
106
+ }, [upgradeApiEndpoint, config, router]);
107
+
112
108
  // 更新价格显示
113
- const updatePriceDisplay = (newBillingType: string) => {
109
+ const updatePriceDisplay = useCallback((newBillingType: string) => {
114
110
  const providerConfig = getActiveProviderConfig(config);
115
111
 
116
112
  data.plans.forEach((plan: any) => {
117
113
  const productConfig = providerConfig.products[plan.key as 'free' | 'pro' | 'ultra'];
118
114
  const pricing = productConfig.plans[newBillingType as 'monthly' | 'yearly'];
119
115
 
120
- // 更新价格值
121
116
  const priceValueElement = document.querySelector(`[data-price-value="${plan.key}"]`) as HTMLElement;
122
117
  if (priceValueElement) {
123
118
  if (pricing.amount === 0) {
@@ -127,14 +122,12 @@ export function MoneyPriceInteractive({
127
122
  }
128
123
  }
129
124
 
130
- // 更新单位
131
125
  const priceUnitElement = document.querySelector(`[data-price-unit="${plan.key}"]`) as HTMLElement;
132
126
  if (priceUnitElement) {
133
127
  const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
134
128
  priceUnitElement.textContent = billingOption?.unit || '/month';
135
129
  }
136
130
 
137
- // 更新原价和折扣
138
131
  const priceOriginalElement = document.querySelector(`[data-price-original="${plan.key}"]`) as HTMLElement;
139
132
  const priceDiscountElement = document.querySelector(`[data-price-discount="${plan.key}"]`) as HTMLElement;
140
133
 
@@ -158,17 +151,16 @@ export function MoneyPriceInteractive({
158
151
  if (priceDiscountElement) priceDiscountElement.style.display = 'none';
159
152
  }
160
153
 
161
- // 更新副标题
162
154
  const priceSubtitleElement = document.querySelector(`[data-price-subtitle="${plan.key}"]`) as HTMLElement;
163
155
  if (priceSubtitleElement && plan.showBillingSubTitle !== false) {
164
156
  const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
165
157
  priceSubtitleElement.textContent = billingOption?.subTitle || '';
166
158
  }
167
159
  });
168
- };
169
-
160
+ }, [config, data]);
161
+
170
162
  // 更新按钮样式
171
- const updateButtonStyles = (newBillingType: string) => {
163
+ const updateButtonStyles = useCallback((newBillingType: string) => {
172
164
  const monthlyButton = document.querySelector('[data-billing-button="monthly"]') as HTMLButtonElement;
173
165
  const yearlyButton = document.querySelector('[data-billing-button="yearly"]') as HTMLButtonElement;
174
166
 
@@ -184,17 +176,16 @@ export function MoneyPriceInteractive({
184
176
  yearlyButton.className = cn('min-w-[120px] px-6 py-2 font-medium transition text-lg relative', activeClasses);
185
177
  }
186
178
  }
187
- };
188
-
179
+ }, []);
180
+
189
181
  // 更新折扣信息
190
- const updateDiscountInfo = (newBillingType: string) => {
182
+ const updateDiscountInfo = useCallback((newBillingType: string) => {
191
183
  const discountInfoElement = document.querySelector('[data-discount-info]') as HTMLElement;
192
184
  if (!discountInfoElement) return;
193
185
 
194
186
  const billingOption = data.billingSwitch.options.find(opt => opt.key === newBillingType);
195
187
  const providerConfig = getActiveProviderConfig(config);
196
188
 
197
- // 检查是否有折扣
198
189
  let hasDiscount = false;
199
190
  let discountPercent = 0;
200
191
 
@@ -207,7 +198,6 @@ export function MoneyPriceInteractive({
207
198
  }
208
199
  });
209
200
 
210
- // 清空内容
211
201
  discountInfoElement.innerHTML = '';
212
202
 
213
203
  if (hasDiscount && billingOption?.discountText) {
@@ -216,29 +206,8 @@ export function MoneyPriceInteractive({
216
206
  discountBadge.textContent = billingOption.discountText.replace('{percent}', String(discountPercent));
217
207
  discountInfoElement.appendChild(discountBadge);
218
208
  }
219
- };
220
-
221
- // 动态替换按钮
222
- useEffect(() => {
223
- data.plans.forEach((plan: any) => {
224
- const placeholder = document.querySelector(`[data-button-placeholder="${plan.key}"]`);
225
- if (placeholder) {
226
- const root = createRoot(placeholder);
227
- root.render(
228
- <MoneyPriceButton
229
- planKey={plan.key}
230
- userContext={userContext}
231
- billingType={billingType}
232
- onLogin={handleLogin}
233
- onUpgrade={handleUpgrade}
234
- texts={data.buttonTexts}
235
- isProcessing={isProcessing}
236
- />
237
- );
238
- }
239
- });
240
- }, [userContext, billingType, isProcessing]);
241
-
209
+ }, [config, data]);
210
+
242
211
  // 处理月付/年付切换和 tooltip 功能
243
212
  useEffect(() => {
244
213
  const monthlyButton = document.querySelector('[data-billing-button="monthly"]') as HTMLButtonElement;
@@ -265,7 +234,6 @@ export function MoneyPriceInteractive({
265
234
  yearlyButton.addEventListener('click', handleYearlyClick);
266
235
  }
267
236
 
268
- // 添加 tooltip 功能
269
237
  const tooltipHandlers: Array<{
270
238
  element: HTMLElement;
271
239
  handlers: {
@@ -307,11 +275,9 @@ export function MoneyPriceInteractive({
307
275
  });
308
276
  });
309
277
 
310
- // 初始化价格显示
311
278
  updatePriceDisplay(billingType);
312
279
  updateDiscountInfo(billingType);
313
280
 
314
- // 清理
315
281
  return () => {
316
282
  if (monthlyButton) {
317
283
  monthlyButton.removeEventListener('click', handleMonthlyClick);
@@ -320,15 +286,14 @@ export function MoneyPriceInteractive({
320
286
  yearlyButton.removeEventListener('click', handleYearlyClick);
321
287
  }
322
288
 
323
- // 清理 tooltip 事件监听器
324
289
  tooltipHandlers.forEach(({ element, handlers }) => {
325
290
  element.removeEventListener('mouseenter', handlers.mouseenter);
326
291
  element.removeEventListener('mousemove', handlers.mousemove);
327
292
  element.removeEventListener('mouseleave', handlers.mouseleave);
328
293
  });
329
294
  };
330
- }, [data]);
331
-
295
+ }, [data, billingType, updatePriceDisplay, updateButtonStyles, updateDiscountInfo]);
296
+
332
297
  // Tooltip 组件
333
298
  const Tooltip = ({ show, content, x, y }: typeof tooltip) => {
334
299
  if (!show) return null;
@@ -351,6 +316,24 @@ export function MoneyPriceInteractive({
351
316
  </div>
352
317
  );
353
318
  };
354
-
355
- return <Tooltip {...tooltip} />;
319
+
320
+ // 直接渲染按钮,确保宽度占满
321
+ return (
322
+ <>
323
+ {data.plans.map((plan: any) => (
324
+ <div key={plan.key} data-button-placeholder={plan.key}>
325
+ <MoneyPriceButton
326
+ planKey={plan.key}
327
+ userContext={userContext}
328
+ billingType={billingType}
329
+ onLogin={handleLogin}
330
+ onUpgrade={handleUpgrade}
331
+ texts={data.buttonTexts}
332
+ isProcessing={isProcessing}
333
+ />
334
+ </div>
335
+ ))}
336
+ <Tooltip {...tooltip} />
337
+ </>
338
+ );
356
339
  }
@@ -20,7 +20,7 @@ export interface UserContext {
20
20
  }
21
21
 
22
22
  // 支付供应商类型
23
- export type PaymentProvider = 'stripe' | 'alipay' | 'wechat' | 'paypal';
23
+ export type PaymentProvider = 'stripe' | 'apple' | 'paypal' | 'wechat' | 'alipay' ;
24
24
 
25
25
  // 价格计划
26
26
  export interface EnhancePricePlan {
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { GradientButton } from '@third-ui/fuma/mdx';
3
2
  import { cn } from '@windrun-huaiin/lib/utils';
4
3
  import { getTranslations } from 'next-intl/server';
5
- import { getActiveProviderConfig } from './money-price-config';
4
+ import { getActiveProviderConfig } from './money-price-config-util';
6
5
  import { MoneyPriceInteractive } from './money-price-interactive';
7
- import type { MoneyPriceData, MoneyPriceProps } from './money-price-types';
6
+ import type { MoneyPriceProps, MoneyPriceData } from './money-price-types';
8
7
 
9
8
  export async function MoneyPrice({
10
9
  locale,
@@ -39,23 +38,19 @@ export async function MoneyPrice({
39
38
  currency: config.display.currency
40
39
  };
41
40
 
42
- // 获取激活的支付供应商配置
43
41
  const providerConfig = getActiveProviderConfig(config);
44
42
  const minPlanFeaturesCount = config.display.minFeaturesCount;
45
-
46
- // 使用默认计费类型进行静态渲染
43
+
47
44
  const defaultBilling = data.billingSwitch.defaultKey;
48
45
  const defaultBillingDisplay = data.billingSwitch.options.find(
49
46
  (opt: any) => opt.key === defaultBilling
50
47
  ) || data.billingSwitch.options[0];
51
48
 
52
- // 计算特性数量
53
49
  const maxFeaturesCount = Math.max(
54
50
  ...data.plans.map((plan: any) => plan.features?.length || 0),
55
51
  minPlanFeaturesCount || 0
56
52
  );
57
53
 
58
- // 处理卡片高度对齐
59
54
  const getFeatureRows = (plan: any) => {
60
55
  const features = plan.features || [];
61
56
  const filled = [...features];
@@ -63,7 +58,6 @@ export async function MoneyPrice({
63
58
  return filled;
64
59
  };
65
60
 
66
- // 静态价格渲染逻辑
67
61
  function renderPrice(plan: any, billingKey = data.billingSwitch.defaultKey) {
68
62
  const productConfig = providerConfig.products[plan.key as 'free' | 'pro' | 'ultra'];
69
63
  const pricing = productConfig.plans[billingKey as 'monthly' | 'yearly'];
@@ -72,7 +66,6 @@ export async function MoneyPrice({
72
66
  ) || defaultBillingDisplay;
73
67
  const billingSubTitle = currentBillingDisplay?.subTitle || '';
74
68
 
75
- // 免费计划
76
69
  if (pricing.amount === 0) {
77
70
  return (
78
71
  <div className="flex flex-col items-start w-full" data-price-container={plan.key}>
@@ -92,7 +85,6 @@ export async function MoneyPrice({
92
85
  );
93
86
  }
94
87
 
95
- // 付费计划
96
88
  const hasDiscount = pricing.discountPercent && pricing.discountPercent > 0;
97
89
  const unit = currentBillingDisplay.unit || '';
98
90
  let discountText = '';
@@ -140,7 +132,6 @@ export async function MoneyPrice({
140
132
 
141
133
  return (
142
134
  <section id="money-pricing" className={cn("px-4 py-10 md:px-16 md:py-16 mx-auto max-w-7xl scroll-mt-10", sectionClassName)}>
143
- {/* 标题和副标题 */}
144
135
  <h2 className="text-3xl md:text-4xl font-bold text-center mb-3">
145
136
  {data.title}
146
137
  </h2>
@@ -148,7 +139,6 @@ export async function MoneyPrice({
148
139
  {data.subtitle}
149
140
  </p>
150
141
 
151
- {/* 计费切换按钮 */}
152
142
  <div className="flex flex-col items-center">
153
143
  <div className="flex items-center relative mb-3">
154
144
  <div className="flex bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-full p-1" data-billing-switch>
@@ -179,12 +169,10 @@ export async function MoneyPrice({
179
169
  </div>
180
170
  </div>
181
171
 
182
- {/* 折扣信息 - 默认计费的静态渲染 */}
183
172
  <div className="h-8 flex items-center justify-center mb-3" data-discount-info>
184
173
  {(() => {
185
174
  const opt = data.billingSwitch.options.find((opt: any) => opt.key === data.billingSwitch.defaultKey);
186
175
 
187
- // 检查默认计费类型是否有折扣
188
176
  let hasDiscount = false;
189
177
  let discountPercent = 0;
190
178
 
@@ -211,7 +199,6 @@ export async function MoneyPrice({
211
199
  </div>
212
200
  </div>
213
201
 
214
- {/* 价格卡片区域 */}
215
202
  <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
216
203
  {data.plans.map((plan: any, _idx: number) => (
217
204
  <div
@@ -224,7 +211,6 @@ export async function MoneyPrice({
224
211
  )}
225
212
  style={{ minHeight: maxFeaturesCount*100 }}
226
213
  >
227
- {/* 标题和标签 */}
228
214
  <div className="flex items-center gap-2 mb-2">
229
215
  <span className="text-xl font-bold text-gray-900 dark:text-gray-100">{plan.title}</span>
230
216
  {plan.titleTags && plan.titleTags.map((tag: string, i: number) => (
@@ -234,14 +220,11 @@ export async function MoneyPrice({
234
220
  ))}
235
221
  </div>
236
222
 
237
- {/* 价格和单位/折扣 */}
238
223
  {renderPrice(plan)}
239
224
 
240
- {/* 特性列表 */}
241
225
  <ul className="flex-1 mb-6 mt-4">
242
226
  {getFeatureRows(plan).map((feature: any, i: number) => (
243
227
  <li key={i} className="flex items-center gap-2 mb-2 min-h-[28px]" data-feature-item={`${plan.key}-${i}`}>
244
- {/* 图标 */}
245
228
  {feature ? (
246
229
  <span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200 mr-1">
247
230
  {feature.icon ? <span>{feature.icon}</span> : <span className="font-bold">✓</span>}
@@ -249,13 +232,11 @@ export async function MoneyPrice({
249
232
  ) : (
250
233
  <span className="inline-flex items-center justify-center w-5 h-5 rounded-full mr-1">&nbsp;</span>
251
234
  )}
252
- {/* 标签 */}
253
235
  {feature && feature.tag && (
254
236
  <span className="px-1 py-0.5 text-[6px] rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 font-semibold align-middle">
255
237
  {feature.tag}
256
238
  </span>
257
239
  )}
258
- {/* 描述 + 提示 */}
259
240
  {feature ? (
260
241
  <span className="relative group cursor-pointer text-sm text-gray-800 dark:text-gray-200">
261
242
  {feature.description}
@@ -279,23 +260,15 @@ export async function MoneyPrice({
279
260
  ))}
280
261
  </ul>
281
262
 
282
- {/* 占位符,确保卡片高度一致 */}
283
263
  <div className="flex-1" />
284
264
 
285
- {/* 按钮占位,客户端会替换 */}
286
- <div data-button-placeholder={plan.key}>
287
- <GradientButton
288
- title={data.buttonTexts.getStarted}
289
- disabled={true}
290
- align="center"
291
- className="w-full"
292
- />
265
+ <div data-button-placeholder={plan.key} className="w-full">
266
+ {/* MoneyPriceInteractive will render the button here */}
293
267
  </div>
294
268
  </div>
295
269
  ))}
296
270
  </div>
297
271
 
298
- {/* 客户端增强组件 */}
299
272
  <MoneyPriceInteractive
300
273
  data={data}
301
274
  config={config}
@@ -12,10 +12,9 @@ export * from './price-plan';
12
12
  // Money Price Server Component and Types
13
13
  export { MoneyPrice } from './money-price/money-price';
14
14
  export {
15
- moneyPriceConfig,
16
15
  getActiveProviderConfig,
17
16
  getProductPricing
18
- } from './money-price/money-price-config';
17
+ } from './money-price/money-price-config-util';
19
18
 
20
19
  // Money Price Types (shared between server and client)
21
20
  export type {