@windrun-huaiin/third-ui 30.1.0 → 31.0.0
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.
- package/README.md +109 -143
- package/dist/ai/ai-prompt-textarea.js +5 -5
- package/dist/ai/ai-prompt-textarea.mjs +5 -5
- package/dist/clerk/clerk-auth-appearance.d.ts +13 -0
- package/dist/clerk/clerk-auth-appearance.js +19 -0
- package/dist/clerk/clerk-auth-appearance.mjs +15 -0
- package/dist/clerk/clerk-auth-modal-appearance.d.ts +12 -0
- package/dist/clerk/clerk-auth-modal-appearance.js +17 -0
- package/dist/clerk/clerk-auth-modal-appearance.mjs +14 -0
- package/dist/clerk/clerk-page-context-generator.js +3 -3
- package/dist/clerk/clerk-page-context-generator.mjs +3 -3
- package/dist/clerk/clerk-page-generator.js +4 -4
- package/dist/clerk/clerk-page-generator.mjs +4 -4
- package/dist/clerk/clerk-user-client.js +2 -1
- package/dist/clerk/clerk-user-client.mjs +2 -1
- package/dist/clerk/fingerprint/fingerprint-client.d.ts +10 -10
- package/dist/clerk/fingerprint/fingerprint-client.js +20 -20
- package/dist/clerk/fingerprint/fingerprint-client.mjs +20 -20
- package/dist/clerk/fingerprint/fingerprint-provider.d.ts +3 -3
- package/dist/clerk/fingerprint/fingerprint-provider.js +8 -8
- package/dist/clerk/fingerprint/fingerprint-provider.mjs +8 -8
- package/dist/clerk/fingerprint/fingerprint-server.d.ts +12 -12
- package/dist/clerk/fingerprint/fingerprint-server.js +17 -17
- package/dist/clerk/fingerprint/fingerprint-server.mjs +17 -17
- package/dist/clerk/fingerprint/fingerprint-shared.d.ts +3 -3
- package/dist/clerk/fingerprint/fingerprint-shared.js +10 -10
- package/dist/clerk/fingerprint/fingerprint-shared.mjs +10 -10
- package/dist/clerk/fingerprint/types.d.ts +0 -1
- package/dist/clerk/fingerprint/use-fingerprint.js +7 -7
- package/dist/clerk/fingerprint/use-fingerprint.mjs +7 -7
- package/dist/clerk/signin-with-fingerprint-client.d.ts +2 -2
- package/dist/clerk/signin-with-fingerprint-client.js +7 -6
- package/dist/clerk/signin-with-fingerprint-client.mjs +7 -6
- package/dist/clerk/signup-button-with-fingerprint-client.js +6 -4
- package/dist/clerk/signup-button-with-fingerprint-client.mjs +6 -4
- package/dist/clerk/signup-with-fingerprint-client.d.ts +2 -2
- package/dist/clerk/signup-with-fingerprint-client.js +7 -6
- package/dist/clerk/signup-with-fingerprint-client.mjs +7 -6
- package/dist/fuma/heavy/mermaid.js +1 -1
- package/dist/fuma/heavy/mermaid.mjs +1 -1
- package/dist/fuma/site-x.js +0 -1
- package/dist/fuma/site-x.mjs +0 -1
- package/dist/main/calendar/calendar-date-range-input.js +1 -1
- package/dist/main/calendar/calendar-date-range-input.mjs +1 -1
- package/dist/main/credit/types.d.ts +8 -8
- package/dist/main/money-price/index.d.ts +1 -1
- package/dist/main/money-price/money-price-button.js +10 -10
- package/dist/main/money-price/money-price-button.mjs +10 -10
- package/dist/main/money-price/money-price-config-util.d.ts +30 -30
- package/dist/main/money-price/money-price-config-util.js +48 -48
- package/dist/main/money-price/money-price-config-util.mjs +48 -48
- package/dist/main/money-price/money-price-interactive.js +30 -18
- package/dist/main/money-price/money-price-interactive.mjs +30 -18
- package/dist/main/money-price/money-price-types.d.ts +7 -1
- package/dist/main/money-price/money-price-types.js +2 -2
- package/dist/main/money-price/money-price-types.mjs +2 -2
- package/dist/main/money-price/server.d.ts +1 -1
- package/dist/main/pill-select/x-pill-select.js +2 -2
- package/dist/main/pill-select/x-pill-select.mjs +2 -2
- package/dist/main/server.d.ts +1 -1
- package/package.json +13 -7
- package/src/ai/ai-prompt-textarea.tsx +6 -6
- package/src/clerk/clerk-auth-appearance.ts +16 -0
- package/src/clerk/clerk-page-context-generator.tsx +3 -5
- package/src/clerk/clerk-page-generator.tsx +9 -8
- package/src/clerk/clerk-user-client.tsx +14 -5
- package/src/clerk/fingerprint/fingerprint-client.ts +20 -20
- package/src/clerk/fingerprint/fingerprint-provider.tsx +11 -11
- package/src/clerk/fingerprint/fingerprint-server.ts +17 -17
- package/src/clerk/fingerprint/fingerprint-shared.ts +10 -10
- package/src/clerk/fingerprint/types.ts +0 -1
- package/src/clerk/fingerprint/use-fingerprint.ts +7 -7
- package/src/clerk/signin-with-fingerprint-client.tsx +7 -7
- package/src/clerk/signup-button-with-fingerprint-client.tsx +7 -5
- package/src/clerk/signup-with-fingerprint-client.tsx +7 -7
- package/src/fuma/base/custom-home-layout.tsx +4 -4
- package/src/fuma/heavy/mermaid.tsx +1 -1
- package/src/fuma/site-x.tsx +0 -1
- package/src/main/calendar/calendar-date-range-input.tsx +1 -1
- package/src/main/credit/types.ts +8 -8
- package/src/main/gallery/gallery-mobile-swiper.tsx +0 -1
- package/src/main/gallery/gallery-server.tsx +2 -2
- package/src/main/money-price/index.ts +2 -0
- package/src/main/money-price/money-price-button.tsx +10 -10
- package/src/main/money-price/money-price-config-util.ts +49 -49
- package/src/main/money-price/money-price-interactive.tsx +40 -20
- package/src/main/money-price/money-price-types.ts +21 -14
- package/src/main/money-price/server.ts +2 -0
- package/src/main/pill-select/x-pill-select.tsx +2 -2
- package/src/main/server.ts +3 -1
- package/src/styles/third-ui.css +8 -0
|
@@ -21,7 +21,7 @@ import { FINGERPRINT_SOURCE_REFER, isDebugFingerprintId, isValidFingerprintId }
|
|
|
21
21
|
* Accepts configuration to customize API endpoint and behavior
|
|
22
22
|
*/
|
|
23
23
|
export function useFingerprint(config: FingerprintConfig): UseFingerprintResult {
|
|
24
|
-
//
|
|
24
|
+
// Server-side rendering guard.
|
|
25
25
|
if (typeof window === 'undefined') {
|
|
26
26
|
return {
|
|
27
27
|
fingerprintId: null,
|
|
@@ -51,7 +51,7 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
|
|
|
51
51
|
}, []);
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* Phase 1: initialize fingerprint ID.
|
|
55
55
|
*/
|
|
56
56
|
const initializeFingerprintId = useCallback(async () => {
|
|
57
57
|
try {
|
|
@@ -68,7 +68,7 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
|
|
|
68
68
|
}, []);
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
71
|
+
* Phase 2: initialize anonymous user.
|
|
72
72
|
*/
|
|
73
73
|
const initializeAnonymousUser = useCallback(async () => {
|
|
74
74
|
if (!fingerprintId) {
|
|
@@ -140,7 +140,7 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
|
|
|
140
140
|
}, [fingerprintId, config.apiEndpoint, isInitialized]);
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
143
|
+
* Refresh user data with a POST request; the backend supports upsert semantics.
|
|
144
144
|
*/
|
|
145
145
|
const refreshUserData = useCallback(async () => {
|
|
146
146
|
if (!fingerprintId) {
|
|
@@ -180,7 +180,7 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
|
|
|
180
180
|
}
|
|
181
181
|
}, [fingerprintId, config.apiEndpoint]);
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Phase 1: generate fingerprint ID after page load.
|
|
184
184
|
useEffect(() => {
|
|
185
185
|
const initFingerprint = async () => {
|
|
186
186
|
setIsLoading(true);
|
|
@@ -191,11 +191,11 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
|
|
|
191
191
|
initFingerprint();
|
|
192
192
|
}, [initializeFingerprintId]);
|
|
193
193
|
|
|
194
|
-
//
|
|
194
|
+
// Phase 2: initialize the user once a fingerprint ID is available; the backend supports upsert semantics.
|
|
195
195
|
useEffect(() => {
|
|
196
196
|
if (!fingerprintId || isInitialized || isLoading || error || config.autoInitialize === false) return;
|
|
197
197
|
|
|
198
|
-
//
|
|
198
|
+
// Use POST directly; the backend handles lookup and create-if-missing behavior.
|
|
199
199
|
initializeAnonymousUser();
|
|
200
200
|
}, [fingerprintId, isInitialized, isLoading, error, initializeAnonymousUser, config.autoInitialize]);
|
|
201
201
|
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import { SignIn } from '@clerk/nextjs';
|
|
4
4
|
import { useEffect } from 'react';
|
|
5
|
+
import { clerkAuthPageAppearance } from './clerk-auth-appearance';
|
|
5
6
|
import { useFingerprintContextSafe } from './fingerprint/fingerprint-provider';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* SignIn component with fingerprint awareness
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Falls back to the standard SignIn component when FingerprintProvider is absent.
|
|
11
|
+
* Handles fingerprint-related metadata when FingerprintProvider is available.
|
|
11
12
|
*/
|
|
12
13
|
export function SignInWithFingerprint() {
|
|
13
14
|
const fingerprintContext = useFingerprintContextSafe();
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
+
// Use defaults when fingerprint context is unavailable.
|
|
16
17
|
const {
|
|
17
18
|
fingerprintId = null,
|
|
18
19
|
xUser = null,
|
|
@@ -20,19 +21,18 @@ export function SignInWithFingerprint() {
|
|
|
20
21
|
initializeAnonymousUser = async () => {}
|
|
21
22
|
} = fingerprintContext || {};
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
+
// Prepare Clerk metadata with anonymous user information.
|
|
24
25
|
const unsafeMetadata = {
|
|
25
26
|
user_id: xUser?.userId || null,
|
|
26
27
|
fingerprint_id: fingerprintId || null,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
+
// Ensure the anonymous user has been initialized.
|
|
30
31
|
useEffect(() => {
|
|
31
32
|
if (!isInitialized && fingerprintId) {
|
|
32
33
|
initializeAnonymousUser();
|
|
33
34
|
}
|
|
34
35
|
}, [fingerprintId, isInitialized, initializeAnonymousUser]);
|
|
35
36
|
|
|
36
|
-
return <SignIn unsafeMetadata={unsafeMetadata} />;
|
|
37
|
+
return <SignIn appearance={clerkAuthPageAppearance} unsafeMetadata={unsafeMetadata} />;
|
|
37
38
|
}
|
|
38
|
-
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { SignUpButton, useClerk } from '@clerk/nextjs';
|
|
4
4
|
import { useEffect } from 'react';
|
|
5
|
+
import { clerkAuthModalAppearance } from './clerk-auth-appearance';
|
|
5
6
|
import { useFingerprintContextSafe } from './fingerprint/fingerprint-provider';
|
|
6
7
|
|
|
7
8
|
interface SignUpButtonWithFingerprintProps {
|
|
@@ -15,7 +16,7 @@ export function SignUpButtonWithFingerprint({
|
|
|
15
16
|
}: SignUpButtonWithFingerprintProps) {
|
|
16
17
|
if (mode === 'redirect') {
|
|
17
18
|
return (
|
|
18
|
-
//
|
|
19
|
+
// Redirect mode navigates directly to the custom sign-up page.
|
|
19
20
|
<SignUpButton>
|
|
20
21
|
<button
|
|
21
22
|
className="w-16 sm:w-20 h-8 sm:h-9 px-1.5 sm:px-2 border border-gray-300 rounded-full hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800 text-center text-xs sm:text-sm whitespace-nowrap"
|
|
@@ -26,10 +27,10 @@ export function SignUpButtonWithFingerprint({
|
|
|
26
27
|
)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
+
// Modal mode requires a custom sign-up button.
|
|
30
31
|
const fingerprintContext = useFingerprintContextSafe();
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Use defaults when fingerprint context is unavailable.
|
|
33
34
|
const {
|
|
34
35
|
fingerprintId = null,
|
|
35
36
|
xUser = null,
|
|
@@ -43,7 +44,7 @@ export function SignUpButtonWithFingerprint({
|
|
|
43
44
|
fingerprint_id: fingerprintId || null,
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
+
// Ensure the anonymous user has been initialized.
|
|
47
48
|
useEffect(() => {
|
|
48
49
|
if (!isInitialized && fingerprintId) {
|
|
49
50
|
initializeAnonymousUser();
|
|
@@ -55,6 +56,7 @@ export function SignUpButtonWithFingerprint({
|
|
|
55
56
|
|
|
56
57
|
const handleClick = () => {
|
|
57
58
|
openSignUp({
|
|
59
|
+
appearance: clerkAuthModalAppearance,
|
|
58
60
|
unsafeMetadata,
|
|
59
61
|
});
|
|
60
62
|
};
|
|
@@ -67,4 +69,4 @@ export function SignUpButtonWithFingerprint({
|
|
|
67
69
|
{signUp}
|
|
68
70
|
</button>
|
|
69
71
|
);
|
|
70
|
-
}
|
|
72
|
+
}
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import { SignUp } from '@clerk/nextjs';
|
|
4
4
|
import { useEffect } from 'react';
|
|
5
|
+
import { clerkAuthPageAppearance } from './clerk-auth-appearance';
|
|
5
6
|
import { useFingerprintContextSafe } from './fingerprint/fingerprint-provider';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* SignUp component with fingerprint awareness
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Falls back to the standard SignUp component when FingerprintProvider is absent.
|
|
11
|
+
* Handles fingerprint-related metadata when FingerprintProvider is available.
|
|
11
12
|
*/
|
|
12
13
|
export function SignUpWithFingerprint() {
|
|
13
14
|
const fingerprintContext = useFingerprintContextSafe();
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
+
// Use defaults when fingerprint context is unavailable.
|
|
16
17
|
const {
|
|
17
18
|
fingerprintId = null,
|
|
18
19
|
xUser = null,
|
|
@@ -20,19 +21,18 @@ export function SignUpWithFingerprint() {
|
|
|
20
21
|
initializeAnonymousUser = async () => {}
|
|
21
22
|
} = fingerprintContext || {};
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
+
// Prepare Clerk metadata with anonymous user information.
|
|
24
25
|
const unsafeMetadata = {
|
|
25
26
|
user_id: xUser?.userId || null,
|
|
26
27
|
fingerprint_id: fingerprintId || null,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
+
// Ensure the anonymous user has been initialized.
|
|
30
31
|
useEffect(() => {
|
|
31
32
|
if (!isInitialized && fingerprintId) {
|
|
32
33
|
initializeAnonymousUser();
|
|
33
34
|
}
|
|
34
35
|
}, [fingerprintId, isInitialized, initializeAnonymousUser]);
|
|
35
36
|
|
|
36
|
-
return <SignUp unsafeMetadata={unsafeMetadata} />;
|
|
37
|
+
return <SignUp appearance={clerkAuthPageAppearance} unsafeMetadata={unsafeMetadata} />;
|
|
37
38
|
}
|
|
38
|
-
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
type MobileMenuAction,
|
|
12
12
|
} from './custom-header';
|
|
13
13
|
|
|
14
|
-
// - bannerHeight/headerHeight
|
|
15
|
-
// - layoutStyle
|
|
16
|
-
// - CustomHomeHeader
|
|
17
|
-
// -
|
|
14
|
+
// - Set bannerHeight/headerHeight to the rem values expected by the project. Use bannerHeight = 0 when there is no banner.
|
|
15
|
+
// - layoutStyle passes the variables to HomeLayout's main element, offsetting content without has-banner/no-banner classes.
|
|
16
|
+
// - CustomHomeHeader accepts HomeLayout props such as links, nav, searchToggle, themeSwitch, and i18n, then reuses Fumadocs navigation behavior.
|
|
17
|
+
// - The banner can still use FumaBannerSuit or any custom banner component. Header positioning and z-index are already handled; the toggle only affects bannerHeight.
|
|
18
18
|
|
|
19
19
|
export interface CustomHomeLayoutProps {
|
|
20
20
|
locale: string;
|
|
@@ -179,7 +179,7 @@ export function Mermaid({ chart, title, watermarkEnabled, watermarkText, handDra
|
|
|
179
179
|
// prevent browser-level zoom (touchpad pinch/shortcut) from taking effect when the dialog is open
|
|
180
180
|
useEffect(() => {
|
|
181
181
|
if (!open) return;
|
|
182
|
-
//
|
|
182
|
+
// Init default zoom out 400%
|
|
183
183
|
resetTransform();
|
|
184
184
|
const onGlobalWheel = (ev: WheelEvent) => {
|
|
185
185
|
if (ev.ctrlKey || ev.metaKey) {
|
package/src/fuma/site-x.tsx
CHANGED
package/src/main/credit/types.ts
CHANGED
|
@@ -47,21 +47,21 @@ export interface CreditCTAConfig {
|
|
|
47
47
|
export type CreditBucketStatus = 'active' | 'expiringSoon' | 'expired';
|
|
48
48
|
|
|
49
49
|
export interface CreditBucket {
|
|
50
|
-
/**
|
|
50
|
+
/** Business-defined credit type identifier for translation mapping or analytics. */
|
|
51
51
|
kind: string;
|
|
52
|
-
/**
|
|
52
|
+
/** Display name override; otherwise the component uses the default translation for kind. */
|
|
53
53
|
label?: string;
|
|
54
|
-
/**
|
|
54
|
+
/** Current credit balance. */
|
|
55
55
|
balance: number;
|
|
56
|
-
/**
|
|
56
|
+
/** Credit limit for this credit type. */
|
|
57
57
|
limit: number;
|
|
58
|
-
/**
|
|
58
|
+
/** Optional status label for highlighting states such as expiration. */
|
|
59
59
|
status?: CreditBucketStatus;
|
|
60
|
-
/**
|
|
60
|
+
/** Credit expiration time as a local time string, used to derive component state. */
|
|
61
61
|
expiresAt?: string;
|
|
62
|
-
/**
|
|
62
|
+
/** Progress percentage from 0 to 100; computed from balance/limit when omitted. */
|
|
63
63
|
progressPercent?: number;
|
|
64
|
-
/**
|
|
64
|
+
/** Additional details, such as remaining days or usage limits. */
|
|
65
65
|
description?: string;
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -19,7 +19,6 @@ const swiperThemeStyle = {
|
|
|
19
19
|
export function GalleryMobileSwiper({ items }: Props) {
|
|
20
20
|
return (
|
|
21
21
|
<div className="block sm:hidden px-4">
|
|
22
|
-
{/* 外层容器:强制 maxWidth,防止任何溢出 */}
|
|
23
22
|
<div
|
|
24
23
|
className="w-full overflow-hidden"
|
|
25
24
|
style={{ maxWidth: "min(calc(100vw - 48px), 350px)", margin: "0 auto" }}
|
|
@@ -33,10 +33,10 @@ export async function Gallery({ locale, sectionClassName, button }: GalleryProps
|
|
|
33
33
|
</h2>
|
|
34
34
|
<p className="text-center max-w-2xl mx-auto mb-16">{data.description}</p>
|
|
35
35
|
|
|
36
|
-
{/*
|
|
36
|
+
{/* Mobile swiper */}
|
|
37
37
|
<GalleryMobileSwiper items={data.items} />
|
|
38
38
|
|
|
39
|
-
{/*
|
|
39
|
+
{/* Desktop grid */}
|
|
40
40
|
<GalleryDesktopGrid items={data.items} />
|
|
41
41
|
|
|
42
42
|
{button && <div className="text-center mx-auto mt-12 max-w-[85vw]">{button}</div>}
|
|
@@ -44,9 +44,9 @@ export function MoneyPriceButton({
|
|
|
44
44
|
return 0;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
//
|
|
47
|
+
// Button configuration for one-time mode.
|
|
48
48
|
const getOnetimeButtonConfig = () => {
|
|
49
|
-
//
|
|
49
|
+
// Anonymous users: show the sign-in button on every card.
|
|
50
50
|
if (!isAuthenticated) {
|
|
51
51
|
return {
|
|
52
52
|
text: texts.getStarted,
|
|
@@ -57,12 +57,12 @@ export function MoneyPriceButton({
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (subscriptionStatus === UserState.Anonymous) {
|
|
60
|
-
//
|
|
60
|
+
// Signed in but status unknown: treat as FreeUser.
|
|
61
61
|
console.warn('Clerk is authed OK but user is anonymous!');
|
|
62
62
|
return { text: '', disabled: true, hidden: true };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
//
|
|
65
|
+
// Signed-in users: show the buy-credits button on every card in one-time mode.
|
|
66
66
|
return {
|
|
67
67
|
text: texts.buyCredits || texts.upgrade,
|
|
68
68
|
onClick: () => onAction(planKey, billingType),
|
|
@@ -71,9 +71,9 @@ export function MoneyPriceButton({
|
|
|
71
71
|
};
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// Button configuration for subscription mode.
|
|
75
75
|
const getSubscriptionButtonConfig = () => {
|
|
76
|
-
//
|
|
76
|
+
// Anonymous users.
|
|
77
77
|
if (!isAuthenticated) {
|
|
78
78
|
const getButtonText = () => {
|
|
79
79
|
switch (planKey) {
|
|
@@ -96,7 +96,7 @@ export function MoneyPriceButton({
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
//
|
|
99
|
+
// Signed-in users.
|
|
100
100
|
switch (subscriptionStatus) {
|
|
101
101
|
case UserState.FreeUser: {
|
|
102
102
|
if (planTier === 'F1') {
|
|
@@ -115,7 +115,7 @@ export function MoneyPriceButton({
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
case UserState.ProUser: {
|
|
118
|
-
//
|
|
118
|
+
// Do not allow downgrades to Free.
|
|
119
119
|
if (planTier === 'F1') {
|
|
120
120
|
return { hidden: true };
|
|
121
121
|
}
|
|
@@ -209,13 +209,13 @@ export function MoneyPriceButton({
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
default:
|
|
212
|
-
//
|
|
212
|
+
// Signed in but status unknown: treat as FreeUser.
|
|
213
213
|
console.warn('Clerk is authed OK but user is anonymous!');
|
|
214
214
|
return { text: '', disabled: true, hidden: true };
|
|
215
215
|
}
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
-
//
|
|
218
|
+
// Main button configuration function.
|
|
219
219
|
const getButtonConfig = () => {
|
|
220
220
|
if (billingType === 'onetime') {
|
|
221
221
|
return getOnetimeButtonConfig();
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Money Price Configuration
|
|
3
|
-
*
|
|
3
|
+
* Pricing component configuration.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { MoneyPriceConfig, PaymentProviderConfig, EnhancePricePlan } from './money-price-types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Get the currently active payment provider configuration.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
11
|
+
* Security design:
|
|
12
|
+
* - The utility layer extracts the active provider configuration from the config.
|
|
13
|
+
* - Only the extracted result is returned; the full config structure is not exposed.
|
|
14
|
+
* - Application-level wrappers hide the config object from callers.
|
|
15
15
|
*
|
|
16
|
-
* @param config - MoneyPriceConfig
|
|
17
|
-
* @returns
|
|
16
|
+
* @param config - MoneyPriceConfig object provided by the application layer.
|
|
17
|
+
* @returns The currently active payment provider configuration.
|
|
18
18
|
*/
|
|
19
19
|
export function getActiveProviderConfigUtil(config: MoneyPriceConfig): PaymentProviderConfig {
|
|
20
20
|
const provider = config.activeProvider;
|
|
21
21
|
return config.paymentProviders[provider];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// Helper: get pricing information for a specific product.
|
|
25
25
|
export function getProductPricing(
|
|
26
26
|
productKey: 'F1' | 'P2' | 'U3',
|
|
27
27
|
billingType: string,
|
|
@@ -30,10 +30,10 @@ export function getProductPricing(
|
|
|
30
30
|
): EnhancePricePlan {
|
|
31
31
|
const providerConfig = config.paymentProviders[provider];
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// For one-time billing, try to resolve pricing from credit packs.
|
|
34
34
|
if (billingType === 'onetime') {
|
|
35
35
|
const creditPacks = providerConfig.creditPackProducts;
|
|
36
|
-
//
|
|
36
|
+
// Use the same product key directly: F1 -> F1, P2 -> P2, U3 -> U3.
|
|
37
37
|
if (creditPacks && creditPacks[productKey]) {
|
|
38
38
|
const pack = creditPacks[productKey];
|
|
39
39
|
return {
|
|
@@ -45,7 +45,7 @@ export function getProductPricing(
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Otherwise resolve pricing from subscription products.
|
|
49
49
|
const products = providerConfig.subscriptionProducts || providerConfig.products;
|
|
50
50
|
if (products && products[productKey] && products[productKey].plans[billingType]) {
|
|
51
51
|
return products[productKey].plans[billingType];
|
|
@@ -54,19 +54,19 @@ export function getProductPricing(
|
|
|
54
54
|
throw new Error(`Product pricing not found for ${productKey} ${billingType}`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// ============
|
|
57
|
+
// ============ Safe utility functions: accept only simple mapping inputs and do not expose config internals ============
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* Get the credit amount for a price ID.
|
|
61
61
|
*
|
|
62
|
-
*
|
|
63
|
-
* -
|
|
64
|
-
* -
|
|
65
|
-
* -
|
|
62
|
+
* Security design:
|
|
63
|
+
* - The utility layer parses the config and extracts only the required data.
|
|
64
|
+
* - Only the query result is returned; the full config structure is not exposed.
|
|
65
|
+
* - Application-level wrappers hide the config object from callers.
|
|
66
66
|
*
|
|
67
|
-
* @param priceId -
|
|
68
|
-
* @param config - MoneyPriceConfig
|
|
69
|
-
* @returns
|
|
67
|
+
* @param priceId - Price ID to query.
|
|
68
|
+
* @param config - MoneyPriceConfig object provided by the application layer.
|
|
69
|
+
* @returns The matching credit amount, or null.
|
|
70
70
|
*/
|
|
71
71
|
export function getCreditsFromPriceIdUtil(
|
|
72
72
|
priceId: string | undefined,
|
|
@@ -76,9 +76,9 @@ export function getCreditsFromPriceIdUtil(
|
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// Iterate through all payment providers.
|
|
80
80
|
for (const provider of Object.values(config.paymentProviders)) {
|
|
81
|
-
//
|
|
81
|
+
// Iterate through subscription products.
|
|
82
82
|
const subscriptionProducts = (
|
|
83
83
|
provider.subscriptionProducts || provider.products
|
|
84
84
|
) as Record<string, any>;
|
|
@@ -96,7 +96,7 @@ export function getCreditsFromPriceIdUtil(
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
//
|
|
99
|
+
// Iterate through credit pack products.
|
|
100
100
|
const creditPacks = provider.creditPackProducts as Record<string, any>;
|
|
101
101
|
if (creditPacks) {
|
|
102
102
|
for (const pack of Object.values(creditPacks)) {
|
|
@@ -112,23 +112,23 @@ export function getCreditsFromPriceIdUtil(
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
*
|
|
115
|
+
* Get price configuration by query parameters.
|
|
116
116
|
*
|
|
117
|
-
*
|
|
118
|
-
* 1.
|
|
119
|
-
* 2.
|
|
120
|
-
* 3.
|
|
117
|
+
* Supported query modes:
|
|
118
|
+
* 1. Query directly by priceId.
|
|
119
|
+
* 2. Query by plan and billingType.
|
|
120
|
+
* 3. Query by plan.
|
|
121
121
|
*
|
|
122
|
-
*
|
|
123
|
-
* -
|
|
124
|
-
* -
|
|
125
|
-
* -
|
|
122
|
+
* Security design:
|
|
123
|
+
* - The utility layer parses the config and extracts only matching data.
|
|
124
|
+
* - Only the query result is returned; the full config structure is not exposed.
|
|
125
|
+
* - Application-level wrappers hide the config object from callers.
|
|
126
126
|
*
|
|
127
|
-
* @param priceId -
|
|
128
|
-
* @param plan -
|
|
129
|
-
* @param billingType -
|
|
130
|
-
* @param config - MoneyPriceConfig
|
|
131
|
-
* @returns
|
|
127
|
+
* @param priceId - Optional price ID to query.
|
|
128
|
+
* @param plan - Optional plan name, such as 'P2' or 'U3'.
|
|
129
|
+
* @param billingType - Optional billing type, such as 'monthly' or 'yearly'.
|
|
130
|
+
* @param config - MoneyPriceConfig object provided by the application layer.
|
|
131
|
+
* @returns The matching price config with derived metadata: priceName, description, and interval.
|
|
132
132
|
*/
|
|
133
133
|
export function getPriceConfigUtil(
|
|
134
134
|
priceId: string | undefined,
|
|
@@ -136,9 +136,9 @@ export function getPriceConfigUtil(
|
|
|
136
136
|
billingType: string | undefined,
|
|
137
137
|
config: MoneyPriceConfig
|
|
138
138
|
): (EnhancePricePlan & { priceName: string; description: string; interval?: string }) | null {
|
|
139
|
-
//
|
|
139
|
+
// Iterate through all payment providers.
|
|
140
140
|
for (const provider of Object.values(config.paymentProviders)) {
|
|
141
|
-
//
|
|
141
|
+
// Iterate through subscription products.
|
|
142
142
|
const subscriptionProducts = (
|
|
143
143
|
provider.subscriptionProducts || provider.products
|
|
144
144
|
) as Record<string, any>;
|
|
@@ -149,8 +149,8 @@ export function getPriceConfigUtil(
|
|
|
149
149
|
for (const [billingKey, planConfig] of Object.entries(product.plans)) {
|
|
150
150
|
const plan_config = planConfig as any;
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
// 1.
|
|
152
|
+
// Matching order by priority.
|
|
153
|
+
// 1. Exact priceId match with highest priority.
|
|
154
154
|
if (priceId && plan_config.priceId === priceId) {
|
|
155
155
|
return {
|
|
156
156
|
...plan_config,
|
|
@@ -160,7 +160,7 @@ export function getPriceConfigUtil(
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// 2.
|
|
163
|
+
// 2. Match by both plan and billingType.
|
|
164
164
|
if (!priceId && plan && billingType) {
|
|
165
165
|
if (productKey === plan && billingKey === billingType) {
|
|
166
166
|
return {
|
|
@@ -172,7 +172,7 @@ export function getPriceConfigUtil(
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
// 3.
|
|
175
|
+
// 3. Match by plan when billingType is empty.
|
|
176
176
|
if (!priceId && !billingType && plan && productKey === plan) {
|
|
177
177
|
return {
|
|
178
178
|
...plan_config,
|
|
@@ -186,13 +186,13 @@ export function getPriceConfigUtil(
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// Iterate through credit pack products.
|
|
190
190
|
const creditPacks = provider.creditPackProducts as Record<string, any>;
|
|
191
191
|
if (creditPacks) {
|
|
192
192
|
for (const [packKey, pack] of Object.entries(creditPacks)) {
|
|
193
193
|
const pack_typed = pack as any;
|
|
194
194
|
|
|
195
|
-
//
|
|
195
|
+
// Credit pack match.
|
|
196
196
|
if (priceId && pack_typed.priceId === priceId) {
|
|
197
197
|
return {
|
|
198
198
|
priceId: pack_typed.priceId,
|
|
@@ -205,7 +205,7 @@ export function getPriceConfigUtil(
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
//
|
|
208
|
+
// Match by plan and one-time billing.
|
|
209
209
|
if (!priceId && plan && billingType === 'onetime') {
|
|
210
210
|
if (packKey === plan) {
|
|
211
211
|
return {
|
|
@@ -220,7 +220,7 @@ export function getPriceConfigUtil(
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// Match by plan; also resolves the first credit pack when billingType is empty.
|
|
224
224
|
if (!priceId && !billingType && plan && packKey === plan) {
|
|
225
225
|
return {
|
|
226
226
|
priceId: pack_typed.priceId,
|
|
@@ -237,4 +237,4 @@ export function getPriceConfigUtil(
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
return null;
|
|
240
|
-
}
|
|
240
|
+
}
|