@billdog.io/react-native 1.0.0-beta.2
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/BillDogReactNative.podspec +30 -0
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/android/build.gradle +99 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogABTestModule.kt +317 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogAnalyticsModule.kt +389 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogModule.kt +817 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogPaywallViewManager.kt +383 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogSurveyModule.kt +427 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogVirtualCurrencyModule.kt +407 -0
- package/android/src/main/kotlin/io/billdog/virtualcurrency/BillDogVirtualCurrencyPackage.kt +44 -0
- package/dist/AdvancedPaywallCache.d.ts +70 -0
- package/dist/AdvancedPaywallCache.js +344 -0
- package/dist/BillDog.d.ts +255 -0
- package/dist/BillDog.js +918 -0
- package/dist/BillDogConfiguration.d.ts +145 -0
- package/dist/BillDogConfiguration.js +229 -0
- package/dist/ComponentRenderer.d.ts +17 -0
- package/dist/ComponentRenderer.js +886 -0
- package/dist/LayoutEngine.d.ts +101 -0
- package/dist/LayoutEngine.js +394 -0
- package/dist/LocalizationResolver.d.ts +99 -0
- package/dist/LocalizationResolver.js +266 -0
- package/dist/NativePaywallView.d.ts +36 -0
- package/dist/NativePaywallView.js +59 -0
- package/dist/PaywallCache.d.ts +74 -0
- package/dist/PaywallCache.js +293 -0
- package/dist/PaywallStateManager.d.ts +62 -0
- package/dist/PaywallStateManager.js +147 -0
- package/dist/PaywallView.d.ts +42 -0
- package/dist/PaywallView.js +275 -0
- package/dist/ProductVariableProvider.d.ts +60 -0
- package/dist/ProductVariableProvider.js +239 -0
- package/dist/VariableResolver.d.ts +120 -0
- package/dist/VariableResolver.js +379 -0
- package/dist/WebViewPaywall.d.ts +75 -0
- package/dist/WebViewPaywall.js +360 -0
- package/dist/animations/presets.d.ts +49 -0
- package/dist/animations/presets.js +367 -0
- package/dist/components/AccoladeCpsComponent.d.ts +13 -0
- package/dist/components/AccoladeCpsComponent.js +248 -0
- package/dist/components/AccordionComponent.d.ts +10 -0
- package/dist/components/AccordionComponent.js +187 -0
- package/dist/components/AnimatedWrapper.d.ts +35 -0
- package/dist/components/AnimatedWrapper.js +277 -0
- package/dist/components/CarouselComponent.d.ts +23 -0
- package/dist/components/CarouselComponent.js +484 -0
- package/dist/components/ChartCurveComponent.d.ts +13 -0
- package/dist/components/ChartCurveComponent.js +333 -0
- package/dist/components/CheckboxComponent.d.ts +9 -0
- package/dist/components/CheckboxComponent.js +172 -0
- package/dist/components/ConfettiComponent.d.ts +9 -0
- package/dist/components/ConfettiComponent.js +172 -0
- package/dist/components/CountdownComponent.d.ts +16 -0
- package/dist/components/CountdownComponent.js +414 -0
- package/dist/components/DelayedXComponent.d.ts +12 -0
- package/dist/components/DelayedXComponent.js +240 -0
- package/dist/components/DrawerModalComponent.d.ts +20 -0
- package/dist/components/DrawerModalComponent.js +286 -0
- package/dist/components/GradientView.d.ts +38 -0
- package/dist/components/GradientView.js +126 -0
- package/dist/components/HapticFeedbackComponent.d.ts +16 -0
- package/dist/components/HapticFeedbackComponent.js +64 -0
- package/dist/components/HorizontalProductsComponent.d.ts +17 -0
- package/dist/components/HorizontalProductsComponent.js +284 -0
- package/dist/components/MultiChoiceGridComponent.d.ts +13 -0
- package/dist/components/MultiChoiceGridComponent.js +234 -0
- package/dist/components/NPSScaleComponent.d.ts +16 -0
- package/dist/components/NPSScaleComponent.js +260 -0
- package/dist/components/NavigationComponent.d.ts +26 -0
- package/dist/components/NavigationComponent.js +302 -0
- package/dist/components/OAuthButtonComponent.d.ts +20 -0
- package/dist/components/OAuthButtonComponent.js +237 -0
- package/dist/components/ProgressLoaderComponent.d.ts +13 -0
- package/dist/components/ProgressLoaderComponent.js +308 -0
- package/dist/components/ProgressStepperComponent.d.ts +18 -0
- package/dist/components/ProgressStepperComponent.js +228 -0
- package/dist/components/SegmentedPickerComponent.d.ts +10 -0
- package/dist/components/SegmentedPickerComponent.js +221 -0
- package/dist/components/ShareButtonsComponent.d.ts +21 -0
- package/dist/components/ShareButtonsComponent.js +265 -0
- package/dist/components/ShareCodeComponent.d.ts +16 -0
- package/dist/components/ShareCodeComponent.js +274 -0
- package/dist/components/SingleChoiceGridComponent.d.ts +13 -0
- package/dist/components/SingleChoiceGridComponent.js +210 -0
- package/dist/components/SliderComponent.d.ts +9 -0
- package/dist/components/SliderComponent.js +263 -0
- package/dist/components/StarRatingInputComponent.d.ts +20 -0
- package/dist/components/StarRatingInputComponent.js +198 -0
- package/dist/components/StickerCompositeComponent.d.ts +13 -0
- package/dist/components/StickerCompositeComponent.js +136 -0
- package/dist/components/TabsComponent.d.ts +19 -0
- package/dist/components/TabsComponent.js +431 -0
- package/dist/components/TextInputComponent.d.ts +18 -0
- package/dist/components/TextInputComponent.js +227 -0
- package/dist/components/TimelineComponent.d.ts +19 -0
- package/dist/components/TimelineComponent.js +104 -0
- package/dist/components/ToggleComponent.d.ts +14 -0
- package/dist/components/ToggleComponent.js +132 -0
- package/dist/components/VerticalProductsComponent.d.ts +17 -0
- package/dist/components/VerticalProductsComponent.js +311 -0
- package/dist/dynamicValues.d.ts +36 -0
- package/dist/dynamicValues.js +142 -0
- package/dist/filters/dateFilters.d.ts +50 -0
- package/dist/filters/dateFilters.js +390 -0
- package/dist/filters/index.d.ts +18 -0
- package/dist/filters/index.js +253 -0
- package/dist/hooks/useOfferings.d.ts +76 -0
- package/dist/hooks/useOfferings.js +99 -0
- package/dist/hooks/usePaywall.d.ts +27 -0
- package/dist/hooks/usePaywall.js +114 -0
- package/dist/hooks/usePlacement.d.ts +34 -0
- package/dist/hooks/usePlacement.js +89 -0
- package/dist/hooks/useSubscription.d.ts +40 -0
- package/dist/hooks/useSubscription.js +97 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +198 -0
- package/dist/models/BillDogError.d.ts +40 -0
- package/dist/models/BillDogError.js +11 -0
- package/dist/models/CacheFetchPolicy.d.ts +8 -0
- package/dist/models/CacheFetchPolicy.js +4 -0
- package/dist/models/CustomProduct.d.ts +32 -0
- package/dist/models/CustomProduct.js +15 -0
- package/dist/models/CustomerInfo.d.ts +17 -0
- package/dist/models/CustomerInfo.js +11 -0
- package/dist/models/Delegate.d.ts +20 -0
- package/dist/models/Delegate.js +11 -0
- package/dist/models/EntitlementInfo.d.ts +19 -0
- package/dist/models/EntitlementInfo.js +2 -0
- package/dist/models/Placement.d.ts +54 -0
- package/dist/models/Placement.js +15 -0
- package/dist/models/PurchaseParams.d.ts +39 -0
- package/dist/models/PurchaseParams.js +60 -0
- package/dist/models/PurchaseResult.d.ts +30 -0
- package/dist/models/PurchaseResult.js +15 -0
- package/dist/models/RestoreResult.d.ts +30 -0
- package/dist/models/RestoreResult.js +22 -0
- package/dist/models/RestoredItem.d.ts +26 -0
- package/dist/models/RestoredItem.js +13 -0
- package/dist/modules/ABTest.d.ts +65 -0
- package/dist/modules/ABTest.js +113 -0
- package/dist/modules/Analytics.d.ts +91 -0
- package/dist/modules/Analytics.js +182 -0
- package/dist/modules/Survey.d.ts +65 -0
- package/dist/modules/Survey.js +82 -0
- package/dist/modules/VirtualCurrency.d.ts +63 -0
- package/dist/modules/VirtualCurrency.js +147 -0
- package/dist/murmur.d.ts +23 -0
- package/dist/murmur.js +104 -0
- package/dist/notifications/LiveActivitiesManager.d.ts +47 -0
- package/dist/notifications/LiveActivitiesManager.js +106 -0
- package/dist/notifications/NotificationScheduler.d.ts +55 -0
- package/dist/notifications/NotificationScheduler.js +199 -0
- package/dist/notifications/PurchaseNotificationHandler.d.ts +24 -0
- package/dist/notifications/PurchaseNotificationHandler.js +55 -0
- package/dist/notifications/useDeviceToken.d.ts +36 -0
- package/dist/notifications/useDeviceToken.js +97 -0
- package/dist/notifications/useLiveActivity.d.ts +39 -0
- package/dist/notifications/useLiveActivity.js +104 -0
- package/dist/review/ReviewAnalytics.d.ts +34 -0
- package/dist/review/ReviewAnalytics.js +73 -0
- package/dist/review/ReviewManager.d.ts +83 -0
- package/dist/review/ReviewManager.js +339 -0
- package/dist/review/ReviewTimingManager.d.ts +82 -0
- package/dist/review/ReviewTimingManager.js +224 -0
- package/dist/survey-core/types.d.ts +215 -0
- package/dist/survey-core/types.js +8 -0
- package/dist/types.d.ts +438 -0
- package/dist/types.js +158 -0
- package/dist/utils/ApiClient.d.ts +65 -0
- package/dist/utils/ApiClient.js +132 -0
- package/dist/utils/CSSConverter.d.ts +49 -0
- package/dist/utils/CSSConverter.js +470 -0
- package/dist/utils/animations.d.ts +41 -0
- package/dist/utils/animations.js +194 -0
- package/dist/utils/calendarIntegration.d.ts +36 -0
- package/dist/utils/calendarIntegration.js +89 -0
- package/dist/utils/configurationResolver.d.ts +23 -0
- package/dist/utils/configurationResolver.js +78 -0
- package/dist/utils/deviceDetection.d.ts +38 -0
- package/dist/utils/deviceDetection.js +59 -0
- package/dist/utils/hapticFeedback.d.ts +17 -0
- package/dist/utils/hapticFeedback.js +53 -0
- package/dist/utils/iconMapper.d.ts +26 -0
- package/dist/utils/iconMapper.js +113 -0
- package/dist/utils/notificationPermissions.d.ts +13 -0
- package/dist/utils/notificationPermissions.js +51 -0
- package/dist/utils/priceFormatting.d.ts +19 -0
- package/dist/utils/priceFormatting.js +99 -0
- package/dist/utils/shareSheet.d.ts +17 -0
- package/dist/utils/shareSheet.js +42 -0
- package/dist/utils/subscriptionUpgrade.d.ts +84 -0
- package/dist/utils/subscriptionUpgrade.js +100 -0
- package/dist/valueUtils.d.ts +48 -0
- package/dist/valueUtils.js +129 -0
- package/dist/xFlowTags.d.ts +18 -0
- package/dist/xFlowTags.js +385 -0
- package/ios/BillDogABTest.m +50 -0
- package/ios/BillDogABTest.swift +188 -0
- package/ios/BillDogAnalytics.m +105 -0
- package/ios/BillDogAnalytics.swift +324 -0
- package/ios/BillDogBridge.m +222 -0
- package/ios/BillDogBridge.swift +715 -0
- package/ios/BillDogLiveActivities.m +41 -0
- package/ios/BillDogLiveActivities.swift +104 -0
- package/ios/BillDogPaywallViewManager.m +39 -0
- package/ios/BillDogPaywallViewManager.swift +361 -0
- package/ios/BillDogSurvey.m +34 -0
- package/ios/BillDogSurvey.swift +257 -0
- package/ios/BillDogVirtualCurrencyBridge.m +63 -0
- package/ios/BillDogVirtualCurrencyBridge.swift +511 -0
- package/junit.xml +25 -0
- package/package.json +61 -0
- package/react-native.config.js +13 -0
- package/src/AdvancedPaywallCache.ts +433 -0
- package/src/BillDog.ts +983 -0
- package/src/BillDogConfiguration.ts +318 -0
- package/src/ComponentRenderer.tsx +1325 -0
- package/src/LayoutEngine.ts +459 -0
- package/src/LocalizationResolver.ts +311 -0
- package/src/NativePaywallView.tsx +132 -0
- package/src/PaywallCache.ts +358 -0
- package/src/PaywallStateManager.ts +154 -0
- package/src/PaywallView.tsx +328 -0
- package/src/ProductVariableProvider.ts +282 -0
- package/src/VariableResolver.ts +516 -0
- package/src/WebViewPaywall.tsx +454 -0
- package/src/animations/presets.ts +421 -0
- package/src/components/AccoladeCpsComponent.tsx +259 -0
- package/src/components/AccordionComponent.tsx +213 -0
- package/src/components/AnimatedWrapper.tsx +316 -0
- package/src/components/CarouselComponent.tsx +615 -0
- package/src/components/ChartCurveComponent.tsx +452 -0
- package/src/components/CheckboxComponent.tsx +185 -0
- package/src/components/ConfettiComponent.tsx +178 -0
- package/src/components/CountdownComponent.tsx +454 -0
- package/src/components/DelayedXComponent.tsx +266 -0
- package/src/components/DrawerModalComponent.tsx +324 -0
- package/src/components/GradientView.tsx +129 -0
- package/src/components/HapticFeedbackComponent.tsx +39 -0
- package/src/components/HorizontalProductsComponent.tsx +329 -0
- package/src/components/MultiChoiceGridComponent.tsx +265 -0
- package/src/components/NPSScaleComponent.tsx +294 -0
- package/src/components/NavigationComponent.tsx +329 -0
- package/src/components/OAuthButtonComponent.tsx +266 -0
- package/src/components/ProgressLoaderComponent.tsx +366 -0
- package/src/components/ProgressStepperComponent.tsx +250 -0
- package/src/components/SegmentedPickerComponent.tsx +245 -0
- package/src/components/ShareButtonsComponent.tsx +299 -0
- package/src/components/ShareCodeComponent.tsx +303 -0
- package/src/components/SingleChoiceGridComponent.tsx +229 -0
- package/src/components/SliderComponent.tsx +280 -0
- package/src/components/StarRatingInputComponent.tsx +223 -0
- package/src/components/StickerCompositeComponent.tsx +168 -0
- package/src/components/TabsComponent.tsx +496 -0
- package/src/components/TextInputComponent.tsx +258 -0
- package/src/components/TimelineComponent.tsx +129 -0
- package/src/components/ToggleComponent.tsx +142 -0
- package/src/components/VerticalProductsComponent.tsx +357 -0
- package/src/dynamicValues.ts +180 -0
- package/src/filters/dateFilters.ts +411 -0
- package/src/filters/index.ts +281 -0
- package/src/hooks/useOfferings.ts +192 -0
- package/src/hooks/usePaywall.ts +154 -0
- package/src/hooks/usePlacement.ts +143 -0
- package/src/hooks/useSubscription.ts +154 -0
- package/src/index.ts +245 -0
- package/src/models/BillDogError.ts +24 -0
- package/src/models/CacheFetchPolicy.ts +13 -0
- package/src/models/CustomProduct.ts +33 -0
- package/src/models/CustomerInfo.ts +26 -0
- package/src/models/Delegate.ts +22 -0
- package/src/models/EntitlementInfo.ts +27 -0
- package/src/models/Placement.ts +50 -0
- package/src/models/PurchaseParams.ts +81 -0
- package/src/models/PurchaseResult.ts +32 -0
- package/src/models/RestoreResult.ts +27 -0
- package/src/models/RestoredItem.ts +25 -0
- package/src/modules/ABTest.ts +131 -0
- package/src/modules/Analytics.ts +227 -0
- package/src/modules/Survey.ts +126 -0
- package/src/modules/VirtualCurrency.ts +223 -0
- package/src/murmur.ts +102 -0
- package/src/notifications/LiveActivitiesManager.ts +140 -0
- package/src/notifications/NotificationScheduler.ts +292 -0
- package/src/notifications/PurchaseNotificationHandler.ts +67 -0
- package/src/notifications/useDeviceToken.ts +116 -0
- package/src/notifications/useLiveActivity.ts +125 -0
- package/src/review/ReviewAnalytics.ts +89 -0
- package/src/review/ReviewManager.ts +355 -0
- package/src/review/ReviewTimingManager.ts +265 -0
- package/src/survey-core/types.ts +244 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/types.ts +680 -0
- package/src/utils/ApiClient.ts +163 -0
- package/src/utils/CSSConverter.ts +552 -0
- package/src/utils/animations.ts +232 -0
- package/src/utils/calendarIntegration.ts +113 -0
- package/src/utils/configurationResolver.ts +106 -0
- package/src/utils/deviceDetection.ts +83 -0
- package/src/utils/hapticFeedback.ts +55 -0
- package/src/utils/iconMapper.ts +102 -0
- package/src/utils/notificationPermissions.ts +48 -0
- package/src/utils/priceFormatting.ts +119 -0
- package/src/utils/shareSheet.ts +55 -0
- package/src/utils/subscriptionUpgrade.ts +183 -0
- package/src/valueUtils.ts +108 -0
- package/src/xFlowTags.ts +463 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaywallView - Main React Native component for rendering paywalls
|
|
3
|
+
*
|
|
4
|
+
* Credentials and API base URL are resolved from BillDogConfiguration.
|
|
5
|
+
* Pass an `apiKey` prop only for backward compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
9
|
+
import { ScrollView, View, StyleSheet, ActivityIndicator, Dimensions } from 'react-native';
|
|
10
|
+
import { PaywallConfiguration, ThemeConfig, PaywallComponent } from './types';
|
|
11
|
+
import { ComponentRenderer } from './ComponentRenderer';
|
|
12
|
+
import { paywallStateManager } from './PaywallStateManager';
|
|
13
|
+
import { usePaywall } from './hooks/usePaywall';
|
|
14
|
+
import { detectDeviceCategory } from './utils/deviceDetection';
|
|
15
|
+
import { ThemeColorResolver } from './LayoutEngine';
|
|
16
|
+
import { LocalizationResolver } from './LocalizationResolver';
|
|
17
|
+
|
|
18
|
+
interface PaywallViewProps {
|
|
19
|
+
/**
|
|
20
|
+
* Paywall identifier to fetch from API
|
|
21
|
+
*/
|
|
22
|
+
paywallIdentifier?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Direct configuration (skips API fetch)
|
|
26
|
+
*/
|
|
27
|
+
configuration?: PaywallConfiguration;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Use BillDogConfiguration.configure({ apiKey }) instead
|
|
31
|
+
*/
|
|
32
|
+
apiKey?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Called when user interacts with buttons
|
|
36
|
+
*/
|
|
37
|
+
onButtonPress?: (componentId: string, action: string) => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Called when paywall is dismissed
|
|
41
|
+
*/
|
|
42
|
+
onDismiss?: () => void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Loading component (optional)
|
|
46
|
+
*/
|
|
47
|
+
LoadingComponent?: React.ComponentType;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Error component (optional)
|
|
51
|
+
*/
|
|
52
|
+
ErrorComponent?: React.ComponentType<{ error: Error }>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const PaywallView: React.FC<PaywallViewProps> = ({
|
|
56
|
+
paywallIdentifier,
|
|
57
|
+
configuration: providedConfiguration,
|
|
58
|
+
apiKey,
|
|
59
|
+
onButtonPress,
|
|
60
|
+
onDismiss,
|
|
61
|
+
LoadingComponent,
|
|
62
|
+
ErrorComponent,
|
|
63
|
+
}) => {
|
|
64
|
+
const [deviceCategory, setDeviceCategory] = useState(detectDeviceCategory());
|
|
65
|
+
|
|
66
|
+
// ── ELEMENT STATE ────────────────────────────────────────────────────────────
|
|
67
|
+
// Components have always called a `stateManager` prop (updateState /
|
|
68
|
+
// subscribeToState / getState / setState) but nothing ever supplied one, and every
|
|
69
|
+
// call site optional-chains — so the whole cross-component state protocol was a
|
|
70
|
+
// silent no-op. Supplying the shared manager here activates it for the tree.
|
|
71
|
+
//
|
|
72
|
+
// Reset DURING FIRST RENDER, not in an effect: React runs child effects BEFORE
|
|
73
|
+
// parent effects, so clearing on mount would wipe the subscriptions the children
|
|
74
|
+
// just registered and leave every component permanently deaf. (Inverted from the
|
|
75
|
+
// native SDKs, where the parent clears first and children subscribe after.) The ref
|
|
76
|
+
// guard keeps it once-per-presentation and StrictMode-safe.
|
|
77
|
+
const stateResetRef = useRef(false);
|
|
78
|
+
if (!stateResetRef.current) {
|
|
79
|
+
paywallStateManager.clearAll();
|
|
80
|
+
stateResetRef.current = true;
|
|
81
|
+
}
|
|
82
|
+
useEffect(() => () => paywallStateManager.clearAll(), []);
|
|
83
|
+
|
|
84
|
+
// Fetch paywall if identifier is provided
|
|
85
|
+
const { configuration, loading, error, refetch } = usePaywall({
|
|
86
|
+
paywallIdentifier,
|
|
87
|
+
apiKey,
|
|
88
|
+
skipFetch: !!providedConfiguration,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Listen for orientation changes
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const subscription = Dimensions.addEventListener('change', () => {
|
|
94
|
+
const newCategory = detectDeviceCategory();
|
|
95
|
+
if (newCategory !== deviceCategory) {
|
|
96
|
+
setDeviceCategory(newCategory);
|
|
97
|
+
if (!providedConfiguration) {
|
|
98
|
+
refetch();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return () => subscription?.remove();
|
|
104
|
+
}, [deviceCategory, providedConfiguration]);
|
|
105
|
+
|
|
106
|
+
// Use provided configuration or fetched configuration
|
|
107
|
+
const activeConfiguration = providedConfiguration || configuration;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Unwrap nested configuration structure
|
|
111
|
+
*/
|
|
112
|
+
const unwrappedConfig = useMemo(() => {
|
|
113
|
+
if (!activeConfiguration) return null;
|
|
114
|
+
const config = activeConfiguration as any;
|
|
115
|
+
|
|
116
|
+
if (config.configuration && typeof config.configuration === 'object') {
|
|
117
|
+
const deviceConfig = config.configuration[deviceCategory] ||
|
|
118
|
+
config.configuration['phone_portrait'] ||
|
|
119
|
+
Object.values(config.configuration)[0];
|
|
120
|
+
if (deviceConfig && typeof deviceConfig === 'object') {
|
|
121
|
+
return {
|
|
122
|
+
...deviceConfig,
|
|
123
|
+
wallConfigTheme: config.wallConfig?.themeConfig,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (config[deviceCategory]?.theme || config[deviceCategory]?.components) {
|
|
129
|
+
return config[deviceCategory];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return config;
|
|
133
|
+
}, [activeConfiguration, deviceCategory]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Comprehensive theme resolution
|
|
137
|
+
*/
|
|
138
|
+
const resolvedTheme = useMemo((): ThemeConfig | undefined => {
|
|
139
|
+
if (!unwrappedConfig) return undefined;
|
|
140
|
+
|
|
141
|
+
const config = unwrappedConfig as any;
|
|
142
|
+
|
|
143
|
+
if (config.theme?.resolvedColors) {
|
|
144
|
+
return {
|
|
145
|
+
...config.theme,
|
|
146
|
+
themingMode: config.theme.themingMode || 'full',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (config.wallConfigTheme?.resolvedColors) {
|
|
151
|
+
return config.wallConfigTheme;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (config.structured?.themes) {
|
|
155
|
+
const structuredTheme = config.structured.themes[deviceCategory] ||
|
|
156
|
+
config.structured.themes['phone_portrait'];
|
|
157
|
+
if (structuredTheme?.resolvedColors) {
|
|
158
|
+
return structuredTheme;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (config.theme?.colors) {
|
|
163
|
+
return {
|
|
164
|
+
primaryColor: config.theme.colors.primary,
|
|
165
|
+
secondaryColor: config.theme.colors.secondary,
|
|
166
|
+
backgroundColor: config.theme.colors.background,
|
|
167
|
+
textColor: config.theme.colors.foreground || config.theme.colors.text,
|
|
168
|
+
cardColor: config.theme.colors.card,
|
|
169
|
+
mutedColor: config.theme.colors.muted,
|
|
170
|
+
borderColor: config.theme.colors.border,
|
|
171
|
+
accentColor: config.theme.colors.accent,
|
|
172
|
+
resolvedColors: config.theme.colors,
|
|
173
|
+
themingMode: config.theme.themingMode || 'full',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return config.theme;
|
|
178
|
+
}, [unwrappedConfig, deviceCategory]);
|
|
179
|
+
|
|
180
|
+
// Initialize ThemeColorResolver with resolved theme colors
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (resolvedTheme) {
|
|
183
|
+
const themeForResolver: Record<string, string> = {};
|
|
184
|
+
|
|
185
|
+
if (resolvedTheme.primaryColor) themeForResolver.primaryColor = resolvedTheme.primaryColor;
|
|
186
|
+
if (resolvedTheme.secondaryColor) themeForResolver.secondaryColor = resolvedTheme.secondaryColor;
|
|
187
|
+
if (resolvedTheme.backgroundColor) themeForResolver.backgroundColor = resolvedTheme.backgroundColor;
|
|
188
|
+
if (resolvedTheme.textColor) themeForResolver.textColor = resolvedTheme.textColor;
|
|
189
|
+
if (resolvedTheme.cardColor) themeForResolver.cardColor = resolvedTheme.cardColor;
|
|
190
|
+
if (resolvedTheme.mutedColor) themeForResolver.mutedColor = resolvedTheme.mutedColor;
|
|
191
|
+
if (resolvedTheme.borderColor) themeForResolver.borderColor = resolvedTheme.borderColor;
|
|
192
|
+
if (resolvedTheme.accentColor) themeForResolver.accentColor = resolvedTheme.accentColor;
|
|
193
|
+
|
|
194
|
+
if (resolvedTheme.resolvedColors) {
|
|
195
|
+
Object.entries(resolvedTheme.resolvedColors).forEach(([key, value]) => {
|
|
196
|
+
if (typeof value === 'string') {
|
|
197
|
+
themeForResolver[key] = value;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ThemeColorResolver.setTheme(themeForResolver);
|
|
203
|
+
console.log('[PaywallView] Theme initialized:', themeForResolver);
|
|
204
|
+
} else {
|
|
205
|
+
ThemeColorResolver.setTheme(null);
|
|
206
|
+
}
|
|
207
|
+
}, [resolvedTheme]);
|
|
208
|
+
|
|
209
|
+
if (loading && !activeConfiguration) {
|
|
210
|
+
if (LoadingComponent) {
|
|
211
|
+
return <LoadingComponent />;
|
|
212
|
+
}
|
|
213
|
+
return (
|
|
214
|
+
<View style={styles.centered}>
|
|
215
|
+
<ActivityIndicator size="large" color="#007AFF" />
|
|
216
|
+
</View>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (error && !activeConfiguration) {
|
|
221
|
+
if (ErrorComponent) {
|
|
222
|
+
return <ErrorComponent error={error} />;
|
|
223
|
+
}
|
|
224
|
+
return <View style={styles.centered} />;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!activeConfiguration) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const backgroundColor = resolvedTheme?.backgroundColor ||
|
|
232
|
+
resolvedTheme?.resolvedColors?.background ||
|
|
233
|
+
'#FFFFFF';
|
|
234
|
+
|
|
235
|
+
// Layout direction — RTL when the dashboard set an explicit
|
|
236
|
+
// `theme.direction: 'rtl'` override, or when the effective locale's
|
|
237
|
+
// language is right-to-left. Mirrors Android `PaywallScreen.kt:431-458`.
|
|
238
|
+
// Applied as a Yoga `direction` style so flex layout mirrors for the
|
|
239
|
+
// whole component tree (row reverses, start/end flip).
|
|
240
|
+
const layoutDirection: 'ltr' | 'rtl' = (() => {
|
|
241
|
+
const explicit = (unwrappedConfig as any)?.theme?.direction
|
|
242
|
+
?? (resolvedTheme as any)?.direction;
|
|
243
|
+
if (explicit === 'rtl') return 'rtl';
|
|
244
|
+
if (explicit === 'ltr') return 'ltr';
|
|
245
|
+
const baseLocale = (activeConfiguration as any)?.baseLocale as string | undefined;
|
|
246
|
+
const effectiveLocale = LocalizationResolver.getEffectiveLocale();
|
|
247
|
+
// A non-base effective locale wins; otherwise fall back to baseLocale.
|
|
248
|
+
const localeForDirection =
|
|
249
|
+
effectiveLocale && effectiveLocale !== baseLocale
|
|
250
|
+
? effectiveLocale
|
|
251
|
+
: (baseLocale || effectiveLocale);
|
|
252
|
+
return LocalizationResolver.isRtlLocale(localeForDirection) ? 'rtl' : 'ltr';
|
|
253
|
+
})();
|
|
254
|
+
|
|
255
|
+
const components: PaywallComponent[] = unwrappedConfig?.components || activeConfiguration?.components || [];
|
|
256
|
+
|
|
257
|
+
const absoluteComponents = components.filter((c: PaywallComponent) => c.absolutePosition);
|
|
258
|
+
const relativeComponents = components.filter((c: PaywallComponent) => !c.absolutePosition);
|
|
259
|
+
|
|
260
|
+
const screenLayout = unwrappedConfig?.theme?.screenLayout;
|
|
261
|
+
const contentPadding = screenLayout?.padding || { top: 16, right: 16, bottom: 16, left: 16 };
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<View style={[styles.container, { backgroundColor, direction: layoutDirection }]}>
|
|
265
|
+
<ScrollView
|
|
266
|
+
style={styles.scrollView}
|
|
267
|
+
contentContainerStyle={[
|
|
268
|
+
styles.content,
|
|
269
|
+
{
|
|
270
|
+
paddingTop: contentPadding.top,
|
|
271
|
+
paddingRight: contentPadding.right,
|
|
272
|
+
paddingBottom: contentPadding.bottom,
|
|
273
|
+
paddingLeft: contentPadding.left,
|
|
274
|
+
gap: screenLayout?.gap || 16,
|
|
275
|
+
}
|
|
276
|
+
]}
|
|
277
|
+
>
|
|
278
|
+
{relativeComponents.map((component) => (
|
|
279
|
+
<ComponentRenderer
|
|
280
|
+
key={component.id}
|
|
281
|
+
stateManager={paywallStateManager}
|
|
282
|
+
component={component}
|
|
283
|
+
configuration={activeConfiguration}
|
|
284
|
+
theme={resolvedTheme}
|
|
285
|
+
onButtonPress={onButtonPress}
|
|
286
|
+
/>
|
|
287
|
+
))}
|
|
288
|
+
</ScrollView>
|
|
289
|
+
|
|
290
|
+
{absoluteComponents.length > 0 && (
|
|
291
|
+
<View style={styles.absoluteContainer} pointerEvents="box-none">
|
|
292
|
+
{absoluteComponents.map((component) => (
|
|
293
|
+
<ComponentRenderer
|
|
294
|
+
key={component.id}
|
|
295
|
+
stateManager={paywallStateManager}
|
|
296
|
+
component={component}
|
|
297
|
+
configuration={activeConfiguration}
|
|
298
|
+
theme={resolvedTheme}
|
|
299
|
+
onButtonPress={onButtonPress}
|
|
300
|
+
/>
|
|
301
|
+
))}
|
|
302
|
+
</View>
|
|
303
|
+
)}
|
|
304
|
+
</View>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const styles = StyleSheet.create({
|
|
309
|
+
container: {
|
|
310
|
+
flex: 1,
|
|
311
|
+
position: 'relative',
|
|
312
|
+
},
|
|
313
|
+
scrollView: {
|
|
314
|
+
flex: 1,
|
|
315
|
+
},
|
|
316
|
+
content: {
|
|
317
|
+
flexGrow: 1,
|
|
318
|
+
},
|
|
319
|
+
centered: {
|
|
320
|
+
flex: 1,
|
|
321
|
+
justifyContent: 'center',
|
|
322
|
+
alignItems: 'center',
|
|
323
|
+
},
|
|
324
|
+
absoluteContainer: {
|
|
325
|
+
...StyleSheet.absoluteFillObject,
|
|
326
|
+
zIndex: 10,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product Variable Provider for BillDog React Native SDK
|
|
3
|
+
*
|
|
4
|
+
* Generates variable maps from packages for use in VariableResolver.
|
|
5
|
+
* Enables {{product.price}}, {{product.period}}, {{selectedProduct.*}}, etc.
|
|
6
|
+
*
|
|
7
|
+
* Port of Android's ProductVariableProvider.kt with 1:1 variable key parity.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface PackageInput {
|
|
11
|
+
identifier: string;
|
|
12
|
+
packageType?: string;
|
|
13
|
+
productId?: string;
|
|
14
|
+
price?: string;
|
|
15
|
+
rawPrice?: number;
|
|
16
|
+
currencyCode?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
subscriptionPeriod?: { value: number; unit: string } | null;
|
|
20
|
+
freeTrialPeriod?: { value: number; unit: string } | null;
|
|
21
|
+
introductoryPrice?: { price: number; priceString: string; period?: { value: number; unit: string } } | null;
|
|
22
|
+
isSubscription?: boolean;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TrialEligibilityInput {
|
|
27
|
+
isEligible: boolean;
|
|
28
|
+
hasUsedTrial?: boolean;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ProductVariableProvider {
|
|
33
|
+
private packages: PackageInput[];
|
|
34
|
+
private trialEligibility?: Record<string, TrialEligibilityInput>;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
packages: PackageInput[],
|
|
38
|
+
trialEligibility?: Record<string, TrialEligibilityInput>
|
|
39
|
+
) {
|
|
40
|
+
this.packages = packages;
|
|
41
|
+
this.trialEligibility = trialEligibility;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Generate variable map for a specific product index */
|
|
45
|
+
getVariablesForProduct(index: number): Record<string, any> {
|
|
46
|
+
const pkg = this.packages[index];
|
|
47
|
+
if (!pkg) return {};
|
|
48
|
+
return this.getVariablesForPackage(pkg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Generate variable map for a specific package (40+ keys) */
|
|
52
|
+
getVariablesForPackage(pkg: PackageInput): Record<string, any> {
|
|
53
|
+
const eligibility = this.trialEligibility?.[pkg.productId ?? pkg.identifier];
|
|
54
|
+
const vars: Record<string, any> = {};
|
|
55
|
+
|
|
56
|
+
// Basic
|
|
57
|
+
vars['product.identifier'] = pkg.identifier;
|
|
58
|
+
vars['product.packageType'] = pkg.packageType ?? 'custom';
|
|
59
|
+
|
|
60
|
+
const hasProduct = pkg.price !== undefined || pkg.rawPrice !== undefined;
|
|
61
|
+
|
|
62
|
+
if (hasProduct) {
|
|
63
|
+
vars['product.price'] = pkg.price ?? '';
|
|
64
|
+
vars['product.rawPrice'] = pkg.rawPrice ?? 0;
|
|
65
|
+
vars['product.currencyCode'] = pkg.currencyCode ?? 'USD';
|
|
66
|
+
vars['product.currencySymbol'] = getCurrencySymbol(pkg.currencyCode ?? 'USD');
|
|
67
|
+
vars['product.title'] = pkg.title ?? '';
|
|
68
|
+
vars['product.description'] = pkg.description ?? '';
|
|
69
|
+
|
|
70
|
+
// Period
|
|
71
|
+
const period = pkg.subscriptionPeriod;
|
|
72
|
+
if (period) {
|
|
73
|
+
vars['product.period'] = toDisplayString(period);
|
|
74
|
+
vars['product.periodShort'] = toShortString(period);
|
|
75
|
+
vars['product.periodDays'] = totalDays(period);
|
|
76
|
+
vars['product.periodUnit'] = period.unit;
|
|
77
|
+
vars['product.periodValue'] = period.value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Calculated prices
|
|
81
|
+
const raw = pkg.rawPrice ?? 0;
|
|
82
|
+
const days = period ? totalDays(period) : 0;
|
|
83
|
+
const daily = days > 0 ? raw / days : raw;
|
|
84
|
+
const weekly = days > 0 ? (raw / days) * 7 : raw;
|
|
85
|
+
const monthly = days > 0 ? raw / (days / 30) : raw;
|
|
86
|
+
const yearly = days > 0 ? (raw / days) * 365 : raw;
|
|
87
|
+
const cc = pkg.currencyCode ?? 'USD';
|
|
88
|
+
|
|
89
|
+
vars['product.dailyPrice'] = formatPrice(daily, cc);
|
|
90
|
+
vars['product.weeklyPrice'] = formatPrice(weekly, cc);
|
|
91
|
+
vars['product.monthlyPrice'] = formatPrice(monthly, cc);
|
|
92
|
+
vars['product.yearlyPrice'] = formatPrice(yearly, cc);
|
|
93
|
+
vars['product.rawDailyPrice'] = daily;
|
|
94
|
+
vars['product.rawWeeklyPrice'] = weekly;
|
|
95
|
+
vars['product.rawMonthlyPrice'] = monthly;
|
|
96
|
+
vars['product.rawYearlyPrice'] = yearly;
|
|
97
|
+
|
|
98
|
+
// Trial
|
|
99
|
+
const trial = pkg.freeTrialPeriod;
|
|
100
|
+
vars['product.hasFreeTrial'] = trial != null;
|
|
101
|
+
if (trial) {
|
|
102
|
+
vars['product.trialPeriod'] = toDisplayString(trial);
|
|
103
|
+
vars['product.trialPeriodDays'] = totalDays(trial);
|
|
104
|
+
vars['product.trialPeriodText'] = formatTrialPeriod(trial);
|
|
105
|
+
} else {
|
|
106
|
+
vars['product.trialPeriod'] = '';
|
|
107
|
+
vars['product.trialPeriodDays'] = 0;
|
|
108
|
+
vars['product.trialPeriodText'] = '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
vars['product.isTrialEligible'] = eligibility?.isEligible ?? (trial != null);
|
|
112
|
+
|
|
113
|
+
// Intro offer
|
|
114
|
+
const intro = pkg.introductoryPrice;
|
|
115
|
+
vars['product.hasIntroOffer'] = intro != null;
|
|
116
|
+
if (intro) {
|
|
117
|
+
vars['product.introPrice'] = intro.priceString;
|
|
118
|
+
vars['product.introPriceRaw'] = intro.price;
|
|
119
|
+
vars['product.introPeriod'] = intro.period ? toDisplayString(intro.period) : '';
|
|
120
|
+
} else {
|
|
121
|
+
vars['product.introPrice'] = '';
|
|
122
|
+
vars['product.introPriceRaw'] = 0;
|
|
123
|
+
vars['product.introPeriod'] = '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// User eligibility
|
|
127
|
+
const isEligibleForIntro = eligibility?.isEligible ?? (trial != null || intro != null);
|
|
128
|
+
const hasUsedTrial = eligibility?.hasUsedTrial ?? false;
|
|
129
|
+
vars['user.isEligibleForIntro'] = isEligibleForIntro;
|
|
130
|
+
vars['user.hasUsedTrial'] = hasUsedTrial;
|
|
131
|
+
vars['user.isEligibleForPromo'] = false;
|
|
132
|
+
|
|
133
|
+
// Offer summary
|
|
134
|
+
const hasAnyOffer = trial != null || intro != null;
|
|
135
|
+
vars['offer.hasAnyOffer'] = hasAnyOffer;
|
|
136
|
+
vars['offer.type'] = trial ? 'free_trial' : intro ? 'intro' : 'none';
|
|
137
|
+
|
|
138
|
+
vars['product.isSubscription'] = pkg.isSubscription ?? (period != null);
|
|
139
|
+
} else {
|
|
140
|
+
// Fallback values
|
|
141
|
+
vars['product.price'] = '';
|
|
142
|
+
vars['product.rawPrice'] = 0;
|
|
143
|
+
vars['product.currencyCode'] = 'USD';
|
|
144
|
+
vars['product.currencySymbol'] = '$';
|
|
145
|
+
vars['product.title'] = '';
|
|
146
|
+
vars['product.description'] = '';
|
|
147
|
+
vars['product.period'] = '';
|
|
148
|
+
vars['product.periodShort'] = '';
|
|
149
|
+
vars['product.periodDays'] = 0;
|
|
150
|
+
vars['product.hasFreeTrial'] = false;
|
|
151
|
+
vars['product.isTrialEligible'] = false;
|
|
152
|
+
vars['product.hasIntroOffer'] = false;
|
|
153
|
+
vars['product.isSubscription'] = false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return vars;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Generate variables for selected product with "selectedProduct." prefix */
|
|
160
|
+
getSelectedProductVariables(selectedIndex: number): Record<string, any> {
|
|
161
|
+
const productVars = this.getVariablesForProduct(selectedIndex);
|
|
162
|
+
const result: Record<string, any> = {};
|
|
163
|
+
for (const [key, value] of Object.entries(productVars)) {
|
|
164
|
+
result[key.replace('product.', 'selectedProduct.')] = value;
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Generate all product variables with index prefix (products.0.*, products.1.*, etc.) */
|
|
170
|
+
getAllProductVariables(): Record<string, any> {
|
|
171
|
+
const vars: Record<string, any> = {};
|
|
172
|
+
this.packages.forEach((pkg, index) => {
|
|
173
|
+
const pkgVars = this.getVariablesForPackage(pkg);
|
|
174
|
+
for (const [key, value] of Object.entries(pkgVars)) {
|
|
175
|
+
const productKey = key.startsWith('product.') ? key.slice(8) : key;
|
|
176
|
+
vars[`products.${index}.${productKey}`] = value;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return vars;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Generate convenience aliases (primary, secondary, monthly, annual, etc.) */
|
|
183
|
+
getConvenienceVariables(): Record<string, any> {
|
|
184
|
+
const vars: Record<string, any> = {};
|
|
185
|
+
|
|
186
|
+
// Primary = first package
|
|
187
|
+
if (this.packages[0]) {
|
|
188
|
+
for (const [key, value] of Object.entries(this.getVariablesForPackage(this.packages[0]))) {
|
|
189
|
+
const k = key.startsWith('product.') ? key.slice(8) : key;
|
|
190
|
+
vars[`primary.${k}`] = value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Secondary = second package
|
|
195
|
+
if (this.packages[1]) {
|
|
196
|
+
for (const [key, value] of Object.entries(this.getVariablesForPackage(this.packages[1]))) {
|
|
197
|
+
const k = key.startsWith('product.') ? key.slice(8) : key;
|
|
198
|
+
vars[`secondary.${k}`] = value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Named packages by type
|
|
203
|
+
for (const pkg of this.packages) {
|
|
204
|
+
const typeKey = pkg.packageType ?? 'custom';
|
|
205
|
+
for (const [key, value] of Object.entries(this.getVariablesForPackage(pkg))) {
|
|
206
|
+
const k = key.startsWith('product.') ? key.slice(8) : key;
|
|
207
|
+
vars[`${typeKey}.${k}`] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return vars;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Create provider from an offering */
|
|
215
|
+
static fromOffering(
|
|
216
|
+
offering: { packages: PackageInput[] },
|
|
217
|
+
trialEligibility?: Record<string, TrialEligibilityInput>
|
|
218
|
+
): ProductVariableProvider {
|
|
219
|
+
return new ProductVariableProvider(offering.packages, trialEligibility);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Helpers
|
|
224
|
+
|
|
225
|
+
function getCurrencySymbol(currencyCode: string): string {
|
|
226
|
+
try {
|
|
227
|
+
return (0).toLocaleString(undefined, {
|
|
228
|
+
style: 'currency', currency: currencyCode, minimumFractionDigits: 0, maximumFractionDigits: 0,
|
|
229
|
+
}).replace(/\d/g, '').trim() || '$';
|
|
230
|
+
} catch {
|
|
231
|
+
return '$';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatPrice(price: number, currencyCode: string): string {
|
|
236
|
+
try {
|
|
237
|
+
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currencyCode }).format(price);
|
|
238
|
+
} catch {
|
|
239
|
+
return `$${price.toFixed(2)}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function totalDays(period: { value: number; unit: string }): number {
|
|
244
|
+
switch (period.unit) {
|
|
245
|
+
case 'day': return period.value;
|
|
246
|
+
case 'week': return period.value * 7;
|
|
247
|
+
case 'month': return period.value * 30;
|
|
248
|
+
case 'year': return period.value * 365;
|
|
249
|
+
default: return period.value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function toDisplayString(period: { value: number; unit: string }): string {
|
|
254
|
+
const u = period.unit;
|
|
255
|
+
const v = period.value;
|
|
256
|
+
const unitStr = v === 1
|
|
257
|
+
? (u === 'day' ? 'day' : u === 'week' ? 'week' : u === 'month' ? 'month' : 'year')
|
|
258
|
+
: (u === 'day' ? 'days' : u === 'week' ? 'weeks' : u === 'month' ? 'months' : 'years');
|
|
259
|
+
return `${v} ${unitStr}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function toShortString(period: { value: number; unit: string }): string {
|
|
263
|
+
switch (period.unit) {
|
|
264
|
+
case 'day': return '/day';
|
|
265
|
+
case 'week': return '/wk';
|
|
266
|
+
case 'month': return '/mo';
|
|
267
|
+
case 'year': return '/yr';
|
|
268
|
+
default: return `/${period.unit}`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatTrialPeriod(period: { value: number; unit: string }): string {
|
|
273
|
+
const days = totalDays(period);
|
|
274
|
+
if (days === 1) return '1-day free trial';
|
|
275
|
+
if (days === 7) return '7-day free trial';
|
|
276
|
+
if (days === 14) return '2-week free trial';
|
|
277
|
+
if (days === 30 || days === 31) return '1-month free trial';
|
|
278
|
+
if (days < 7) return `${days}-day free trial`;
|
|
279
|
+
if (days % 7 === 0) return `${days / 7}-week free trial`;
|
|
280
|
+
if (days % 30 === 0) return `${days / 30}-month free trial`;
|
|
281
|
+
return `${days}-day free trial`;
|
|
282
|
+
}
|