@windrun-huaiin/third-ui 31.3.6 → 31.4.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.
Files changed (34) hide show
  1. package/dist/main/credit/credit-nav-button.d.ts +3 -2
  2. package/dist/main/credit/credit-nav-button.js +7 -9
  3. package/dist/main/credit/credit-nav-button.mjs +5 -7
  4. package/dist/main/credit/credit-overview-client.js +6 -3
  5. package/dist/main/credit/credit-overview-client.mjs +7 -4
  6. package/dist/main/credit/credit-overview-nav-client.d.ts +12 -2
  7. package/dist/main/credit/credit-overview-nav-client.js +21 -5
  8. package/dist/main/credit/credit-overview-nav-client.mjs +21 -5
  9. package/dist/main/credit/index.d.ts +1 -1
  10. package/dist/main/credit/types.d.ts +1 -0
  11. package/dist/main/money-price/index.d.ts +2 -0
  12. package/dist/main/money-price/index.js +4 -0
  13. package/dist/main/money-price/index.mjs +1 -0
  14. package/dist/main/money-price/money-price-interactive.d.ts +1 -1
  15. package/dist/main/money-price/money-price-interactive.js +7 -5
  16. package/dist/main/money-price/money-price-interactive.mjs +7 -5
  17. package/dist/main/money-price/money-price-types.d.ts +2 -0
  18. package/dist/main/money-price/money-price.d.ts +1 -1
  19. package/dist/main/money-price/money-price.js +2 -2
  20. package/dist/main/money-price/money-price.mjs +2 -2
  21. package/dist/main/money-price/pricing-modal.d.ts +33 -0
  22. package/dist/main/money-price/pricing-modal.js +109 -0
  23. package/dist/main/money-price/pricing-modal.mjs +105 -0
  24. package/package.json +3 -3
  25. package/src/main/credit/credit-nav-button.tsx +15 -56
  26. package/src/main/credit/credit-overview-client.tsx +36 -25
  27. package/src/main/credit/credit-overview-nav-client.tsx +51 -4
  28. package/src/main/credit/index.ts +1 -0
  29. package/src/main/credit/types.ts +1 -0
  30. package/src/main/money-price/index.ts +10 -0
  31. package/src/main/money-price/money-price-interactive.tsx +41 -31
  32. package/src/main/money-price/money-price-types.ts +2 -0
  33. package/src/main/money-price/money-price.tsx +2 -0
  34. package/src/main/money-price/pricing-modal.tsx +283 -0
@@ -44,6 +44,7 @@ export function MoneyPriceInteractive({
44
44
  customerPortalApiEndpoint,
45
45
  enableClerkModal = false,
46
46
  enabledBillingTypes,
47
+ mobileBillingSwitchBehavior = 'static',
47
48
  enableSubscriptionUpgrade = true,
48
49
  initialBillingType,
49
50
  disableAutoDetectBilling = false,
@@ -71,6 +72,7 @@ export function MoneyPriceInteractive({
71
72
  return acc;
72
73
  }, {});
73
74
  }, [billingOptions]);
75
+ const showBillingSwitch = billingOptions.length > 1;
74
76
  const defaultBilling = useMemo<BillingType>(() => {
75
77
  const defaultKey = data.billingSwitch.defaultKey;
76
78
 
@@ -395,41 +397,49 @@ export function MoneyPriceInteractive({
395
397
  return selectedBillingOption.discountText.replace('{percent}', String(discountPercent));
396
398
  }, [selectedBillingOption, providerConfig, billingType]);
397
399
 
398
- // Configure the mobile floating style for BillingTypeButton.
400
+ // Configure the mobile behavior for BillingTypeButton.
399
401
  return (
400
402
  <>
401
- <div className="flex justify-center mb-6 max-md:sticky max-md:top-30 max-md:z-30 max-md:py-2 max-md:bg-transparent">
402
- <div className="inline-flex bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-full px-2 py-2 sm:px-3 sm:py-3 max-md:w-full max-md:max-w-[340px] max-md:mx-auto shadow-sm" data-billing-switch>
403
- {billingOptions.map(option => {
404
- const isActive = option.key === billingType;
405
- const buttonClasses = isActive
406
- ? cn('text-white rounded-full shadow-sm', themeButtonGradientClass, themeButtonGradientHoverClass )
407
- : 'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full';
408
- const showBadge = option.key === billingType && !!discountBadgeText;
409
-
410
- return (
411
- <div key={option.key} className="relative flex items-center justify-center mx-1">
412
- {showBadge && (
413
- <span className="absolute z-10 left-1/2 -translate-x-1/2 -top-3 sm:-top-4 translate-y-[-50%] px-3 py-0.5 text-[0.625rem] sm:text-xs rounded-md bg-yellow-100 text-yellow-800 font-semibold shadow-sm whitespace-nowrap">
414
- {discountBadgeText}
415
- </span>
416
- )}
417
- <button
418
- className={cn(
419
- 'text-sm md:text-base font-medium transition relative text-center z-10 px-2 sm:px-4 py-2 min-w-[100px] sm:min-w-[120px]',
420
- buttonClasses
403
+ {showBillingSwitch ? (
404
+ <div
405
+ className={cn(
406
+ 'mb-6 flex justify-center',
407
+ mobileBillingSwitchBehavior === 'sticky' &&
408
+ 'max-md:sticky max-md:top-2 max-md:z-30 max-md:py-2 max-md:bg-transparent',
409
+ )}
410
+ >
411
+ <div className="inline-flex max-w-[calc(100vw-2rem)] bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-full px-2 py-2 sm:px-3 sm:py-3 shadow-sm" data-billing-switch>
412
+ {billingOptions.map(option => {
413
+ const isActive = option.key === billingType;
414
+ const buttonClasses = isActive
415
+ ? cn('text-white rounded-full shadow-sm', themeButtonGradientClass, themeButtonGradientHoverClass )
416
+ : 'text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 rounded-full';
417
+ const showBadge = option.key === billingType && !!discountBadgeText;
418
+
419
+ return (
420
+ <div key={option.key} className="relative flex items-center justify-center mx-1">
421
+ {showBadge && (
422
+ <span className="absolute z-10 left-1/2 -translate-x-1/2 -top-3 sm:-top-4 translate-y-[-50%] px-3 py-0.5 text-[0.625rem] sm:text-xs rounded-md bg-yellow-100 text-yellow-800 font-semibold shadow-sm whitespace-nowrap">
423
+ {discountBadgeText}
424
+ </span>
421
425
  )}
422
- type="button"
423
- data-billing-button={option.key}
424
- onClick={() => setUserSelectedBillingType(option.key)}
425
- >
426
- {option.name}
427
- </button>
428
- </div>
429
- );
430
- })}
426
+ <button
427
+ className={cn(
428
+ 'text-sm md:text-base font-medium transition relative text-center z-10 px-2 sm:px-4 py-2 min-w-[88px] min-[360px]:min-w-[100px] sm:min-w-[120px]',
429
+ buttonClasses
430
+ )}
431
+ type="button"
432
+ data-billing-button={option.key}
433
+ onClick={() => setUserSelectedBillingType(option.key)}
434
+ >
435
+ {option.name}
436
+ </button>
437
+ </div>
438
+ );
439
+ })}
440
+ </div>
431
441
  </div>
432
- </div>
442
+ ) : null}
433
443
 
434
444
  <div className="w-full">
435
445
  <div className="flex flex-wrap justify-center gap-5 md:gap-6 xl:gap-8 w-full max-w-6xl mx-auto">
@@ -102,6 +102,7 @@ export interface MoneyPriceProps {
102
102
  enableClerkModal?: boolean;
103
103
  sectionClassName?: string;
104
104
  enabledBillingTypes?: string[];
105
+ mobileBillingSwitchBehavior?: 'sticky' | 'static';
105
106
  enableSubscriptionUpgrade?: boolean;
106
107
  initUserContext?: InitUserContext;
107
108
  initialBillingType?: string;
@@ -115,6 +116,7 @@ export interface MoneyPriceInteractiveProps {
115
116
  customerPortalApiEndpoint?: string;
116
117
  enableClerkModal?: boolean;
117
118
  enabledBillingTypes?: string[];
119
+ mobileBillingSwitchBehavior?: 'sticky' | 'static';
118
120
  enableSubscriptionUpgrade?: boolean;
119
121
  initialBillingType?: string;
120
122
  disableAutoDetectBilling?: boolean;
@@ -11,6 +11,7 @@ export async function MoneyPrice({
11
11
  enableClerkModal = false,
12
12
  sectionClassName,
13
13
  enabledBillingTypes,
14
+ mobileBillingSwitchBehavior,
14
15
  enableSubscriptionUpgrade = true,
15
16
  initUserContext,
16
17
  initialBillingType,
@@ -36,6 +37,7 @@ export async function MoneyPrice({
36
37
  customerPortalApiEndpoint={customerPortalApiEndpoint}
37
38
  enableClerkModal={enableClerkModal}
38
39
  enabledBillingTypes={enabledBillingTypes}
40
+ mobileBillingSwitchBehavior={mobileBillingSwitchBehavior}
39
41
  enableSubscriptionUpgrade={enableSubscriptionUpgrade}
40
42
  initUserContext={initUserContext}
41
43
  initialBillingType={initialBillingType}
@@ -0,0 +1,283 @@
1
+ 'use client';
2
+
3
+ import { XIcon } from '@windrun-huaiin/base-ui/icons';
4
+ import { themeMainBgColor } from '@windrun-huaiin/base-ui/lib';
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogContent,
8
+ AlertDialogHeader,
9
+ AlertDialogTitle,
10
+ } from '@windrun-huaiin/base-ui/ui';
11
+ import { cn } from '@windrun-huaiin/lib/utils';
12
+ import {
13
+ createContext,
14
+ useCallback,
15
+ useContext,
16
+ useEffect,
17
+ useMemo,
18
+ useRef,
19
+ useState,
20
+ type ReactNode,
21
+ } from 'react';
22
+ import { dialogThemedOverlayClass } from '../alert-dialog/dialog-styles';
23
+ import { MoneyPriceInteractive } from './money-price-interactive';
24
+ import type {
25
+ InitUserContext,
26
+ MoneyPriceConfig,
27
+ MoneyPriceData,
28
+ } from './money-price-types';
29
+
30
+ export interface OpenPricingModalOptions {
31
+ billingType?: string;
32
+ }
33
+
34
+ export interface PricingModalProps {
35
+ open: boolean;
36
+ onOpenChange: (open: boolean) => void;
37
+ data: MoneyPriceData;
38
+ config: MoneyPriceConfig;
39
+ billingType?: string;
40
+ checkoutApiEndpoint?: string;
41
+ customerPortalApiEndpoint?: string;
42
+ enableClerkModal?: boolean;
43
+ enabledBillingTypes?: string[];
44
+ mobileBillingSwitchBehavior?: 'sticky' | 'static';
45
+ enableSubscriptionUpgrade?: boolean;
46
+ initUserContext?: InitUserContext;
47
+ isInitLoading?: boolean;
48
+ }
49
+
50
+ export interface PricingModalProviderProps
51
+ extends Omit<PricingModalProps, 'open' | 'onOpenChange' | 'billingType'> {
52
+ children: ReactNode;
53
+ pricingContextEndpoint?: string;
54
+ }
55
+
56
+ interface PricingModalContextValue {
57
+ openPricingModal: (options?: OpenPricingModalOptions) => void;
58
+ closePricingModal: () => void;
59
+ isPricingModalOpen: boolean;
60
+ }
61
+
62
+ const emptyInitUserContext: InitUserContext = {
63
+ fingerprintId: null,
64
+ xUser: null,
65
+ xCredit: null,
66
+ xSubscription: null,
67
+ isClerkAuthenticated: false,
68
+ };
69
+
70
+ const PricingModalContext = createContext<PricingModalContextValue | null>(null);
71
+
72
+ function normalizeEnabledBillingTypes(
73
+ data: MoneyPriceData,
74
+ enabledBillingTypes: string[] | undefined,
75
+ ) {
76
+ const availableKeys = data.billingSwitch.options.map((option) => option.key);
77
+ const configuredKeys = enabledBillingTypes?.length ? enabledBillingTypes : availableKeys;
78
+
79
+ return Array.from(
80
+ new Set(
81
+ configuredKeys
82
+ .map((billingType) => billingType.trim())
83
+ .filter((billingType) => availableKeys.includes(billingType)),
84
+ ),
85
+ );
86
+ }
87
+
88
+ function resolveBillingType({
89
+ data,
90
+ enabledBillingTypes,
91
+ requestedBillingType,
92
+ }: {
93
+ data: MoneyPriceData;
94
+ enabledBillingTypes?: string[];
95
+ requestedBillingType?: string;
96
+ }) {
97
+ const enabledKeys = normalizeEnabledBillingTypes(data, enabledBillingTypes);
98
+
99
+ if (requestedBillingType && enabledKeys.includes(requestedBillingType)) {
100
+ return requestedBillingType;
101
+ }
102
+
103
+ if (enabledKeys.includes(data.billingSwitch.defaultKey)) {
104
+ return data.billingSwitch.defaultKey;
105
+ }
106
+
107
+ return enabledKeys[0] ?? data.billingSwitch.options[0]?.key;
108
+ }
109
+
110
+ export function PricingModal({
111
+ open,
112
+ onOpenChange,
113
+ data,
114
+ config,
115
+ billingType,
116
+ checkoutApiEndpoint,
117
+ customerPortalApiEndpoint,
118
+ enableClerkModal = false,
119
+ enabledBillingTypes,
120
+ mobileBillingSwitchBehavior,
121
+ enableSubscriptionUpgrade = true,
122
+ initUserContext,
123
+ isInitLoading = false,
124
+ }: PricingModalProps) {
125
+ const resolvedBillingType = useMemo(
126
+ () =>
127
+ resolveBillingType({
128
+ data,
129
+ enabledBillingTypes,
130
+ requestedBillingType: billingType,
131
+ }),
132
+ [billingType, data, enabledBillingTypes],
133
+ );
134
+
135
+ return (
136
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
137
+ {open ? (
138
+ <AlertDialogContent
139
+ className={cn(
140
+ 'mt-5 sm:mt-6 md:mt-10 lg:mt-15 w-[95vw] max-w-[1200px] overflow-hidden border border-slate-200 p-0 shadow-[0_32px_90px_rgba(15,23,42,0.25)] ring-1 ring-black/5 dark:border-white/12 dark:shadow-[0_40px_120px_rgba(0,0,0,0.6)] dark:ring-white/10',
141
+ themeMainBgColor,
142
+ )}
143
+ overlayClassName={dialogThemedOverlayClass}
144
+ >
145
+ <AlertDialogHeader className="flex flex-row items-center justify-between border-b border-slate-200 px-6 pt-4 pb-1 dark:border-slate-800">
146
+ <AlertDialogTitle asChild>
147
+ <div className="flex flex-wrap items-baseline gap-3 text-slate-900 dark:text-white">
148
+ <span className="text-2xl font-semibold leading-tight">
149
+ {data.title}
150
+ </span>
151
+ {data.subtitle ? (
152
+ <span className="text-sm font-medium text-slate-500 dark:text-slate-300">
153
+ {data.subtitle}
154
+ </span>
155
+ ) : null}
156
+ </div>
157
+ </AlertDialogTitle>
158
+ <button
159
+ type="button"
160
+ className="rounded-full p-2 text-gray-400 transition hover:bg-gray-400 hover:text-gray-400 dark:text-white/80 dark:hover:bg-white/80 dark:hover:text-white/80"
161
+ onClick={() => onOpenChange(false)}
162
+ >
163
+ <XIcon className="h-6 w-6" />
164
+ </button>
165
+ </AlertDialogHeader>
166
+ <div className="max-h-[60vh] overflow-y-auto px-4 pt-2 pb-6 sm:max-h-[80vh]">
167
+ <div className="mx-auto w-full">
168
+ <MoneyPriceInteractive
169
+ key={resolvedBillingType ?? 'default'}
170
+ data={data}
171
+ config={config}
172
+ checkoutApiEndpoint={checkoutApiEndpoint}
173
+ customerPortalApiEndpoint={customerPortalApiEndpoint}
174
+ enableClerkModal={enableClerkModal}
175
+ enabledBillingTypes={enabledBillingTypes}
176
+ mobileBillingSwitchBehavior={mobileBillingSwitchBehavior}
177
+ enableSubscriptionUpgrade={enableSubscriptionUpgrade}
178
+ initialBillingType={resolvedBillingType}
179
+ disableAutoDetectBilling={resolvedBillingType === 'onetime'}
180
+ initUserContext={initUserContext}
181
+ isInitLoading={isInitLoading}
182
+ />
183
+ </div>
184
+ </div>
185
+ </AlertDialogContent>
186
+ ) : null}
187
+ </AlertDialog>
188
+ );
189
+ }
190
+
191
+ export function PricingModalProvider({
192
+ children,
193
+ pricingContextEndpoint,
194
+ initUserContext: initUserContextProp,
195
+ isInitLoading: isInitLoadingProp = false,
196
+ ...modalProps
197
+ }: PricingModalProviderProps) {
198
+ const [open, setOpen] = useState(false);
199
+ const [requestedBillingType, setRequestedBillingType] = useState<string | undefined>();
200
+ const [loadedInitUserContext, setLoadedInitUserContext] = useState<InitUserContext>(
201
+ initUserContextProp ?? emptyInitUserContext,
202
+ );
203
+ const [isContextLoading, setIsContextLoading] = useState(false);
204
+ const loadedContextRef = useRef(!!initUserContextProp);
205
+
206
+ useEffect(() => {
207
+ if (initUserContextProp) {
208
+ loadedContextRef.current = true;
209
+ setLoadedInitUserContext(initUserContextProp);
210
+ }
211
+ }, [initUserContextProp]);
212
+
213
+ const loadPricingContext = useCallback(async () => {
214
+ if (!pricingContextEndpoint || loadedContextRef.current) {
215
+ return;
216
+ }
217
+
218
+ setIsContextLoading(true);
219
+ try {
220
+ const response = await fetch(pricingContextEndpoint, {
221
+ credentials: 'same-origin',
222
+ });
223
+
224
+ if (!response.ok) {
225
+ return;
226
+ }
227
+
228
+ const nextContext = (await response.json()) as InitUserContext;
229
+ loadedContextRef.current = true;
230
+ setLoadedInitUserContext(nextContext);
231
+ } catch (error) {
232
+ console.warn('[PricingModalProvider] Failed to load pricing context', error);
233
+ } finally {
234
+ setIsContextLoading(false);
235
+ }
236
+ }, [pricingContextEndpoint]);
237
+
238
+ const openPricingModal = useCallback(
239
+ (options?: OpenPricingModalOptions) => {
240
+ setRequestedBillingType(options?.billingType);
241
+ setOpen(true);
242
+ void loadPricingContext();
243
+ },
244
+ [loadPricingContext],
245
+ );
246
+
247
+ const closePricingModal = useCallback(() => {
248
+ setOpen(false);
249
+ }, []);
250
+
251
+ const contextValue = useMemo(
252
+ () => ({
253
+ openPricingModal,
254
+ closePricingModal,
255
+ isPricingModalOpen: open,
256
+ }),
257
+ [closePricingModal, open, openPricingModal],
258
+ );
259
+
260
+ return (
261
+ <PricingModalContext.Provider value={contextValue}>
262
+ {children}
263
+ <PricingModal
264
+ {...modalProps}
265
+ open={open}
266
+ onOpenChange={setOpen}
267
+ billingType={requestedBillingType}
268
+ initUserContext={initUserContextProp ?? loadedInitUserContext}
269
+ isInitLoading={isInitLoadingProp || isContextLoading}
270
+ />
271
+ </PricingModalContext.Provider>
272
+ );
273
+ }
274
+
275
+ export function usePricingModal() {
276
+ const context = useContext(PricingModalContext);
277
+
278
+ if (!context) {
279
+ throw new Error('usePricingModal must be used within PricingModalProvider.');
280
+ }
281
+
282
+ return context;
283
+ }