@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,1325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Renderer - Maps PaywallComponent types to React Native components
|
|
3
|
+
* Enhanced with CSS-first rendering, gradient support, and theme-aware color resolution
|
|
4
|
+
* UPDATED: Comprehensive theme support and fixed stack alignment
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
Text,
|
|
11
|
+
Image,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
ScrollView,
|
|
14
|
+
StyleSheet,
|
|
15
|
+
ViewStyle,
|
|
16
|
+
ImageStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { PaywallComponent, PaywallConfiguration, ThemeConfig } from './types';
|
|
19
|
+
import { VariableResolver } from './VariableResolver';
|
|
20
|
+
import { LayoutEngine, ThemeColorResolver } from './LayoutEngine';
|
|
21
|
+
import { processFullComponent } from './dynamicValues';
|
|
22
|
+
import { PaywallStateManager } from './PaywallStateManager';
|
|
23
|
+
import { BillDog } from './BillDog';
|
|
24
|
+
import { getDeviceInfo } from './utils/deviceDetection';
|
|
25
|
+
import { CountdownComponent } from './components/CountdownComponent';
|
|
26
|
+
import { CarouselComponent } from './components/CarouselComponent';
|
|
27
|
+
import { AccordionComponent } from './components/AccordionComponent';
|
|
28
|
+
import { SliderComponent } from './components/SliderComponent';
|
|
29
|
+
import { ToggleComponent } from './components/ToggleComponent';
|
|
30
|
+
import { CheckboxComponent } from './components/CheckboxComponent';
|
|
31
|
+
import { SegmentedPickerComponent } from './components/SegmentedPickerComponent';
|
|
32
|
+
import { ConfettiComponent } from './components/ConfettiComponent';
|
|
33
|
+
import { HapticFeedbackComponent } from './components/HapticFeedbackComponent';
|
|
34
|
+
import { DelayedXComponent } from './components/DelayedXComponent';
|
|
35
|
+
import { ProgressLoaderComponent } from './components/ProgressLoaderComponent';
|
|
36
|
+
import { ChartCurveComponent } from './components/ChartCurveComponent';
|
|
37
|
+
import { SingleChoiceGridComponent } from './components/SingleChoiceGridComponent';
|
|
38
|
+
import { MultiChoiceGridComponent } from './components/MultiChoiceGridComponent';
|
|
39
|
+
import { DrawerModalComponent } from './components/DrawerModalComponent';
|
|
40
|
+
import { TimelineComponent } from './components/TimelineComponent';
|
|
41
|
+
import { TextInputComponent } from './components/TextInputComponent';
|
|
42
|
+
import { StarRatingInputComponent } from './components/StarRatingInputComponent';
|
|
43
|
+
import { NPSScaleComponent } from './components/NPSScaleComponent';
|
|
44
|
+
import { OAuthButtonComponent } from './components/OAuthButtonComponent';
|
|
45
|
+
import { ProgressStepperComponent } from './components/ProgressStepperComponent';
|
|
46
|
+
import { ShareCodeComponent } from './components/ShareCodeComponent';
|
|
47
|
+
import { ShareButtonsComponent } from './components/ShareButtonsComponent';
|
|
48
|
+
import { VerticalProductsComponent } from './components/VerticalProductsComponent';
|
|
49
|
+
import { HorizontalProductsComponent } from './components/HorizontalProductsComponent';
|
|
50
|
+
import { AccoladeCpsComponent } from './components/AccoladeCpsComponent';
|
|
51
|
+
import { StickerCompositeComponent } from './components/StickerCompositeComponent';
|
|
52
|
+
|
|
53
|
+
import { AnimatedWrapper } from './components/AnimatedWrapper';
|
|
54
|
+
import { NavigationComponent } from './components/NavigationComponent';
|
|
55
|
+
import { TabsComponent } from './components/TabsComponent';
|
|
56
|
+
import { GradientView, isGradientSupported } from './components/GradientView';
|
|
57
|
+
import { GradientConfig } from './utils/CSSConverter';
|
|
58
|
+
|
|
59
|
+
interface ComponentRendererProps {
|
|
60
|
+
component: PaywallComponent;
|
|
61
|
+
configuration: PaywallConfiguration;
|
|
62
|
+
theme?: ThemeConfig;
|
|
63
|
+
onButtonPress?: (componentId: string, action: string) => void;
|
|
64
|
+
stateManager?: any;
|
|
65
|
+
isPreviewMode?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const ComponentRenderer: React.FC<ComponentRendererProps> = ({
|
|
69
|
+
component,
|
|
70
|
+
configuration,
|
|
71
|
+
theme,
|
|
72
|
+
onButtonPress,
|
|
73
|
+
stateManager,
|
|
74
|
+
isPreviewMode = true,
|
|
75
|
+
}) => {
|
|
76
|
+
// Process full component to evaluate dynamic values across ALL property bags.
|
|
77
|
+
//
|
|
78
|
+
// Device variables MUST be merged in. iOS (WallManager) and Android (PaywallManager) both enrich the
|
|
79
|
+
// bag with `buildDeviceVariables` before resolving dynamic values; React Native was the only native SDK
|
|
80
|
+
// that did not, so every `device.*` condition (device.osName, device.platform, …) silently resolved to
|
|
81
|
+
// its default here. buildDeviceVariables emits the canonical flat keys (`device.osName`) the audience
|
|
82
|
+
// authors target; explicit `configuration.variables` win on any collision.
|
|
83
|
+
// Live product selection (Phase 2b). Seeded from the store, then kept current via a
|
|
84
|
+
// subscription, so `products.selectedIndex` / `selectedProduct.*` conditions resolve
|
|
85
|
+
// against what the user actually picked instead of being pinned to the first package.
|
|
86
|
+
// Falls back to 0 whenever no state manager is supplied (e.g. standalone renders),
|
|
87
|
+
// which is exactly the previous behaviour.
|
|
88
|
+
const [liveSelectedIndex, setLiveSelectedIndex] = useState(0);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!stateManager) return;
|
|
91
|
+
const key = PaywallStateManager.SELECTED_INDEX_KEY;
|
|
92
|
+
const initial = stateManager.getState?.(key);
|
|
93
|
+
if (initial !== undefined && initial !== null) setLiveSelectedIndex(Number(initial) || 0);
|
|
94
|
+
return stateManager.subscribeToState?.(key, (value: any) => {
|
|
95
|
+
setLiveSelectedIndex(Number(value) || 0);
|
|
96
|
+
});
|
|
97
|
+
}, [stateManager]);
|
|
98
|
+
|
|
99
|
+
const [customerInfo, setCustomerInfo] = useState(() => BillDog.customerInfoEmitter.latest);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const sub = BillDog.customerInfoEmitter.addListener((info) => {
|
|
102
|
+
setCustomerInfo(info);
|
|
103
|
+
});
|
|
104
|
+
return () => {
|
|
105
|
+
sub.remove();
|
|
106
|
+
};
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const resolvedComponent = useMemo(() => {
|
|
110
|
+
// Full offering-aware render bag (device + product/offer/user + user.seed/
|
|
111
|
+
// subscription) — parity with iOS/Android Phase 2. Was device + config.variables
|
|
112
|
+
// only, so every product/user DV condition silently resolved to `default`.
|
|
113
|
+
// appUserId + customerInfo are read here (sync) and threaded in to avoid an
|
|
114
|
+
// import cycle.
|
|
115
|
+
const bag = VariableResolver.buildRenderVariables(configuration, getDeviceInfo(), {
|
|
116
|
+
selectedIndex: liveSelectedIndex,
|
|
117
|
+
appUserId: BillDog.getAppUserId(),
|
|
118
|
+
customerInfo: customerInfo,
|
|
119
|
+
});
|
|
120
|
+
const allVariables = { ...bag, ...(configuration.variables || {}) };
|
|
121
|
+
return processFullComponent(component, allVariables);
|
|
122
|
+
}, [component, configuration.variables, (configuration as any).packages, liveSelectedIndex, customerInfo]);
|
|
123
|
+
|
|
124
|
+
// Create a VariableResolver with full localization support (must be before early returns)
|
|
125
|
+
const variableResolver = useMemo(() => {
|
|
126
|
+
return VariableResolver.withLocalization(configuration);
|
|
127
|
+
}, [configuration]);
|
|
128
|
+
|
|
129
|
+
// Resolve variables in text using the full resolver (must be before early returns)
|
|
130
|
+
const resolveVariable = useCallback((text: string): string => {
|
|
131
|
+
if (!text) return '';
|
|
132
|
+
return variableResolver.resolve(text);
|
|
133
|
+
}, [variableResolver]);
|
|
134
|
+
|
|
135
|
+
const processedProperties = resolvedComponent.properties || {};
|
|
136
|
+
|
|
137
|
+
// Don't render if not visible (resolved from dynamic values)
|
|
138
|
+
if (resolvedComponent.visible === false || processedProperties.visible === false) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get combined style with absolute position support and custom CSS
|
|
143
|
+
const style = LayoutEngine.getComponentStyle(
|
|
144
|
+
resolvedComponent.layout,
|
|
145
|
+
resolvedComponent.appearance,
|
|
146
|
+
resolvedComponent.size,
|
|
147
|
+
resolvedComponent.type,
|
|
148
|
+
processedProperties,
|
|
149
|
+
resolvedComponent.absolutePosition,
|
|
150
|
+
resolvedComponent.zIndex,
|
|
151
|
+
processedProperties.customCSS
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Theme-aware color resolution
|
|
156
|
+
* Resolves CSS variables and color objects using the provided theme
|
|
157
|
+
*/
|
|
158
|
+
const resolveColor = (color: any, fallback?: string): string => {
|
|
159
|
+
if (!color) return fallback || 'transparent';
|
|
160
|
+
|
|
161
|
+
if (typeof color === 'string') {
|
|
162
|
+
// Handle CSS variable references
|
|
163
|
+
if (color.includes('var(--')) {
|
|
164
|
+
return resolveCSSVariable(color) || fallback || 'transparent';
|
|
165
|
+
}
|
|
166
|
+
return color;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Handle object format
|
|
170
|
+
if (typeof color === 'object') {
|
|
171
|
+
// Check for resolvedColor first (pre-computed by web builder)
|
|
172
|
+
if (color.resolvedColor) {
|
|
173
|
+
return resolveColor(color.resolvedColor, fallback);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (color.type === 'color' && color.color) {
|
|
177
|
+
return resolveColor(color.color, fallback);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (color.type === 'gradient' && color.gradient?.stops?.length) {
|
|
181
|
+
const firstStop = color.gradient.stops[0];
|
|
182
|
+
if (firstStop?.color) {
|
|
183
|
+
return resolveColor(firstStop.color, fallback);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// HSL format
|
|
188
|
+
if (color.type === 'hsl' || (color.h !== undefined && color.s !== undefined && color.l !== undefined)) {
|
|
189
|
+
const h = color.h ?? 0;
|
|
190
|
+
const s = color.s ?? 0;
|
|
191
|
+
const l = color.l ?? 0;
|
|
192
|
+
const a = color.a ?? color.opacity ?? 1;
|
|
193
|
+
if (a < 1) {
|
|
194
|
+
return `hsla(${h}, ${s}%, ${l}%, ${a})`;
|
|
195
|
+
}
|
|
196
|
+
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// RGBA format
|
|
200
|
+
if (color.type === 'rgba' || (color.r !== undefined && color.g !== undefined && color.b !== undefined)) {
|
|
201
|
+
const r = color.r ?? 0;
|
|
202
|
+
const g = color.g ?? 0;
|
|
203
|
+
const b = color.b ?? 0;
|
|
204
|
+
const a = color.a ?? color.opacity ?? 1;
|
|
205
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (color.hex) return color.hex;
|
|
209
|
+
if (color.value) return color.value;
|
|
210
|
+
if (color.color && typeof color.color === 'string') return color.color;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return fallback || 'transparent';
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolve CSS variable to actual color value using theme
|
|
218
|
+
* UPDATED: Handle themingMode disabled
|
|
219
|
+
*/
|
|
220
|
+
const resolveCSSVariable = (cssVar: string): string | undefined => {
|
|
221
|
+
const varMatch = cssVar.match(/var\(--([a-zA-Z-]+)\)/);
|
|
222
|
+
if (!varMatch) return cssVar;
|
|
223
|
+
|
|
224
|
+
const varName = varMatch[1];
|
|
225
|
+
|
|
226
|
+
// Extract opacity if present
|
|
227
|
+
const opacityMatch = cssVar.match(/\/\s*([\d.]+)\s*\)?$/);
|
|
228
|
+
const opacity = opacityMatch ? parseFloat(opacityMatch[1]) : 1;
|
|
229
|
+
|
|
230
|
+
// If theming is disabled, use default colors only
|
|
231
|
+
if (theme?.themingMode === 'disabled') {
|
|
232
|
+
const defaultColor = ThemeColorResolver.getDefaultVariableColor(varName);
|
|
233
|
+
return opacity < 1 ? applyOpacity(defaultColor, opacity) : defaultColor;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// First try theme's resolvedColors (direct lookup)
|
|
237
|
+
if (theme?.resolvedColors?.[varName]) {
|
|
238
|
+
const color = parseHexColor(theme.resolvedColors[varName]);
|
|
239
|
+
return opacity < 1 ? applyOpacity(color, opacity) : color;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Then try ThemeColorResolver (which was initialized with theme)
|
|
243
|
+
const resolved = ThemeColorResolver.resolveVariable(varName);
|
|
244
|
+
if (resolved) {
|
|
245
|
+
const parsedResolved = parseHexColor(resolved);
|
|
246
|
+
return opacity < 1 ? applyOpacity(parsedResolved, opacity) : parsedResolved;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fallback to defaults
|
|
250
|
+
const defaultColor = ThemeColorResolver.getDefaultVariableColor(varName);
|
|
251
|
+
return opacity < 1 ? applyOpacity(defaultColor, opacity) : defaultColor;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parse 8-character hex colors (#AARRGGBB format) to rgba
|
|
256
|
+
*/
|
|
257
|
+
const parseHexColor = (hex: string): string => {
|
|
258
|
+
if (!hex || typeof hex !== 'string') return hex;
|
|
259
|
+
|
|
260
|
+
// Handle 8-character hex with alpha prefix (#AARRGGBB format)
|
|
261
|
+
if (hex.length === 9 && hex.startsWith('#')) {
|
|
262
|
+
const alpha = parseInt(hex.substring(1, 3), 16) / 255;
|
|
263
|
+
const r = parseInt(hex.substring(3, 5), 16);
|
|
264
|
+
const g = parseInt(hex.substring(5, 7), 16);
|
|
265
|
+
const b = parseInt(hex.substring(7, 9), 16);
|
|
266
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(2)})`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return hex;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Apply opacity to a color
|
|
274
|
+
*/
|
|
275
|
+
const applyOpacity = (color: string, opacity: number): string => {
|
|
276
|
+
if (color.startsWith('#')) {
|
|
277
|
+
const cleanHex = color.replace('#', '');
|
|
278
|
+
if (cleanHex.length >= 6) {
|
|
279
|
+
const r = parseInt(cleanHex.substring(0, 2), 16);
|
|
280
|
+
const g = parseInt(cleanHex.substring(2, 4), 16);
|
|
281
|
+
const b = parseInt(cleanHex.substring(4, 6), 16);
|
|
282
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (color.startsWith('hsl(')) {
|
|
287
|
+
const match = color.match(/hsl\(\s*([\d.]+)\s*,?\s*([\d.]+)%?\s*,?\s*([\d.]+)%?\s*\)/);
|
|
288
|
+
if (match) {
|
|
289
|
+
return `hsla(${match[1]}, ${match[2]}%, ${match[3]}%, ${opacity})`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return color;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Universal children rendering function for composite components
|
|
298
|
+
* Renders children in a stack layout (horizontal, vertical, or layered)
|
|
299
|
+
*/
|
|
300
|
+
const renderWithChildren = (component: PaywallComponent): JSX.Element => {
|
|
301
|
+
if (!component.children?.length) {
|
|
302
|
+
return <View />;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Build container style from component's containerLayout - uses canonical 'direction'
|
|
306
|
+
const containerLayout = (component as any).containerLayout || { direction: 'vertical' };
|
|
307
|
+
const containerStyle: ViewStyle = {
|
|
308
|
+
flexDirection: containerLayout.direction === 'horizontal' ? 'row' : 'column',
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Add gap (React Native doesn't support gap directly, use margin on children)
|
|
312
|
+
if (containerLayout.gap) {
|
|
313
|
+
// Will need to add margin to children instead
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add alignment
|
|
317
|
+
if (containerLayout.verticalAlignment) {
|
|
318
|
+
containerStyle.alignItems = containerLayout.verticalAlignment === 'center' ? 'center'
|
|
319
|
+
: containerLayout.verticalAlignment === 'end' ? 'flex-end'
|
|
320
|
+
: 'flex-start';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (containerLayout.horizontalAlignment) {
|
|
324
|
+
containerStyle.justifyContent = containerLayout.horizontalAlignment === 'center' ? 'center'
|
|
325
|
+
: containerLayout.horizontalAlignment === 'end' ? 'flex-end'
|
|
326
|
+
: containerLayout.horizontalAlignment === 'space-between' ? 'space-between'
|
|
327
|
+
: 'flex-start';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<View style={[containerStyle, style]}>
|
|
332
|
+
{component.children.map((child) => (
|
|
333
|
+
<ComponentRenderer
|
|
334
|
+
key={child.id}
|
|
335
|
+
component={child}
|
|
336
|
+
configuration={configuration}
|
|
337
|
+
theme={theme}
|
|
338
|
+
onButtonPress={onButtonPress}
|
|
339
|
+
stateManager={stateManager}
|
|
340
|
+
isPreviewMode={isPreviewMode}
|
|
341
|
+
/>
|
|
342
|
+
))}
|
|
343
|
+
</View>
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Render child component helper for accordion etc.
|
|
348
|
+
const renderChild = (child: PaywallComponent) => (
|
|
349
|
+
<ComponentRenderer
|
|
350
|
+
key={child.id}
|
|
351
|
+
component={child}
|
|
352
|
+
configuration={configuration}
|
|
353
|
+
theme={theme}
|
|
354
|
+
onButtonPress={onButtonPress}
|
|
355
|
+
stateManager={stateManager}
|
|
356
|
+
isPreviewMode={isPreviewMode}
|
|
357
|
+
/>
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Animation wrapper helper
|
|
361
|
+
const wrapWithAnimation = (content: JSX.Element): JSX.Element => {
|
|
362
|
+
const animationMode = processedProperties.animationMode;
|
|
363
|
+
const animationPreset = processedProperties.animationPreset;
|
|
364
|
+
|
|
365
|
+
if (animationMode === 'none' || !animationPreset) {
|
|
366
|
+
return content;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<AnimatedWrapper
|
|
371
|
+
preset={animationPreset}
|
|
372
|
+
trigger={animationMode === 'loop' ? 'loop' : 'enter'}
|
|
373
|
+
duration={processedProperties.animationDuration}
|
|
374
|
+
delay={processedProperties.animationDelay}
|
|
375
|
+
easing={processedProperties.animationEasing}
|
|
376
|
+
>
|
|
377
|
+
{content}
|
|
378
|
+
</AnimatedWrapper>
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Helper to map alignment strings to flex values
|
|
384
|
+
* FIXED: Proper mapping for both axes
|
|
385
|
+
*/
|
|
386
|
+
const mapAlignment = (align?: string): 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'space-between' => {
|
|
387
|
+
switch (align) {
|
|
388
|
+
case 'start': return 'flex-start';
|
|
389
|
+
case 'center': return 'center';
|
|
390
|
+
case 'end': return 'flex-end';
|
|
391
|
+
case 'stretch': return 'stretch';
|
|
392
|
+
case 'space-between': return 'space-between';
|
|
393
|
+
default: return 'flex-start';
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Helper to get gap value from containerLayout
|
|
398
|
+
const getGapValue = (gap: any): number => {
|
|
399
|
+
if (typeof gap === 'number') return gap;
|
|
400
|
+
if (gap && typeof gap === 'object' && 'value' in gap) return gap.value;
|
|
401
|
+
return 8;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Helper to parse color values (HSL, hex, rgba, gradient, or object format)
|
|
405
|
+
const parseColorValue = (color: any): string => {
|
|
406
|
+
return resolveColor(color, 'transparent');
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// Helper to parse size values (number or { value, unit } format)
|
|
410
|
+
const parseSizeValue = (size: any): number | undefined => {
|
|
411
|
+
if (typeof size === 'number') return size;
|
|
412
|
+
if (size && typeof size === 'object' && 'value' in size) return size.value;
|
|
413
|
+
return undefined;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Helper to extract gradient config from backgroundColor
|
|
417
|
+
const extractGradientConfig = (backgroundColor: any): GradientConfig | null => {
|
|
418
|
+
if (!backgroundColor || typeof backgroundColor !== 'object') return null;
|
|
419
|
+
|
|
420
|
+
if (backgroundColor.type === 'gradient' && backgroundColor.gradient) {
|
|
421
|
+
const gradient = backgroundColor.gradient;
|
|
422
|
+
return {
|
|
423
|
+
type: gradient.type || 'linear',
|
|
424
|
+
angle: gradient.angle,
|
|
425
|
+
stops: (gradient.stops || []).map((stop: any) => {
|
|
426
|
+
const rawPos = stop.position !== undefined ? stop.position : stop.offset;
|
|
427
|
+
let posD = 0;
|
|
428
|
+
if (typeof rawPos === 'number') {
|
|
429
|
+
posD = rawPos;
|
|
430
|
+
} else if (typeof rawPos === 'string') {
|
|
431
|
+
const trimmed = rawPos.trim();
|
|
432
|
+
if (trimmed.endsWith('%')) {
|
|
433
|
+
const val = parseFloat(trimmed.slice(0, -1));
|
|
434
|
+
posD = isNaN(val) ? 0 : val / 100;
|
|
435
|
+
} else {
|
|
436
|
+
const val = parseFloat(trimmed);
|
|
437
|
+
posD = isNaN(val) ? 0 : val;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const position = posD <= 1 ? posD : posD / 100;
|
|
441
|
+
return {
|
|
442
|
+
color: parseColorValue(stop.color),
|
|
443
|
+
position,
|
|
444
|
+
};
|
|
445
|
+
}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return null;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get text color from properties or theme
|
|
454
|
+
* Falls back to theme's text color if not specified
|
|
455
|
+
*/
|
|
456
|
+
const getTextColor = (): string => {
|
|
457
|
+
// Check component properties first
|
|
458
|
+
if (processedProperties.color) {
|
|
459
|
+
return resolveColor(processedProperties.color);
|
|
460
|
+
}
|
|
461
|
+
if (processedProperties.textColor) {
|
|
462
|
+
return resolveColor(processedProperties.textColor);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Fall back to theme text color
|
|
466
|
+
if (theme?.textColor) {
|
|
467
|
+
return theme.textColor;
|
|
468
|
+
}
|
|
469
|
+
if (theme?.resolvedColors?.foreground) {
|
|
470
|
+
return theme.resolvedColors.foreground;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Default fallback
|
|
474
|
+
return '#1A1A2E';
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// Render based on component type
|
|
478
|
+
switch (component.type) {
|
|
479
|
+
case 'text':
|
|
480
|
+
const textColor = getTextColor();
|
|
481
|
+
return (
|
|
482
|
+
<Text style={[styles.text, style, { color: textColor }]}>
|
|
483
|
+
{resolveVariable(processedProperties.text || '')}
|
|
484
|
+
</Text>
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
case 'image': {
|
|
488
|
+
const imgUrl = processedProperties.imageUrl
|
|
489
|
+
|| processedProperties.src
|
|
490
|
+
|| processedProperties.source
|
|
491
|
+
|| '';
|
|
492
|
+
const imgObjectFit = processedProperties.objectFit || 'cover';
|
|
493
|
+
const imgAlt = processedProperties.altText || processedProperties.alt || '';
|
|
494
|
+
|
|
495
|
+
if (imgUrl) {
|
|
496
|
+
return (
|
|
497
|
+
<Image
|
|
498
|
+
source={{ uri: imgUrl }}
|
|
499
|
+
// `style` is a shared ViewStyle whose `overflow: 'scroll'` is not valid
|
|
500
|
+
// for ImageStyle; cast to ImageStyle (RN ignores the extra value).
|
|
501
|
+
style={[styles.image, style as ImageStyle]}
|
|
502
|
+
resizeMode={imgObjectFit as 'cover' | 'contain' | 'stretch' | 'center'}
|
|
503
|
+
accessibilityLabel={imgAlt}
|
|
504
|
+
/>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return (
|
|
508
|
+
<View style={[styles.mediaPlaceholder, style]}>
|
|
509
|
+
<Text style={styles.placeholderIcon}>🖼</Text>
|
|
510
|
+
<Text style={styles.placeholderText}>No image</Text>
|
|
511
|
+
</View>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
case 'stack': {
|
|
516
|
+
// Enhanced stack rendering with containerLayout support
|
|
517
|
+
// Uses canonical 'direction' property
|
|
518
|
+
const containerLayout = (component as any).containerLayout || {};
|
|
519
|
+
const direction = containerLayout.direction || 'vertical';
|
|
520
|
+
const gap = getGapValue(containerLayout.gap);
|
|
521
|
+
const isHorizontal = direction === 'horizontal';
|
|
522
|
+
|
|
523
|
+
// Layered mode (similar to CSS Grid stacking)
|
|
524
|
+
if (direction === 'layered') {
|
|
525
|
+
return (
|
|
526
|
+
<View style={[style, { position: 'relative' }]}>
|
|
527
|
+
{component.children?.map((child, index) => {
|
|
528
|
+
// Size-based inference: fill+fill = non-measuring background layer
|
|
529
|
+
const childSize = (child as any).size;
|
|
530
|
+
const isBackground = childSize?.width?.type === 'fill' && childSize?.height?.type === 'fill';
|
|
531
|
+
return (
|
|
532
|
+
<View
|
|
533
|
+
key={child.id}
|
|
534
|
+
// mapAlignment can yield 'space-between' which is valid for
|
|
535
|
+
// justifyContent but not the narrower alignItems FlexAlignType;
|
|
536
|
+
// cast to ViewStyle (RN ignores an out-of-range alignItems).
|
|
537
|
+
style={{
|
|
538
|
+
position: isBackground ? 'absolute' : 'relative',
|
|
539
|
+
top: isBackground ? 0 : undefined,
|
|
540
|
+
left: isBackground ? 0 : undefined,
|
|
541
|
+
right: isBackground ? 0 : undefined,
|
|
542
|
+
bottom: isBackground ? 0 : undefined,
|
|
543
|
+
zIndex: isBackground ? 0 : index + 1,
|
|
544
|
+
alignItems: mapAlignment(containerLayout.horizontalAlignment),
|
|
545
|
+
justifyContent: mapAlignment(containerLayout.verticalAlignment),
|
|
546
|
+
} as ViewStyle}
|
|
547
|
+
>
|
|
548
|
+
<ComponentRenderer
|
|
549
|
+
key={child.id}
|
|
550
|
+
component={child}
|
|
551
|
+
configuration={configuration}
|
|
552
|
+
theme={theme}
|
|
553
|
+
onButtonPress={onButtonPress}
|
|
554
|
+
stateManager={stateManager}
|
|
555
|
+
isPreviewMode={isPreviewMode}
|
|
556
|
+
/>
|
|
557
|
+
</View>
|
|
558
|
+
);
|
|
559
|
+
})}
|
|
560
|
+
</View>
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Direction-aware alignment mapping
|
|
566
|
+
* For horizontal direction (row):
|
|
567
|
+
* - horizontalAlignment controls justifyContent (main axis)
|
|
568
|
+
* - verticalAlignment controls alignItems (cross axis)
|
|
569
|
+
* For vertical direction (column):
|
|
570
|
+
* - verticalAlignment controls justifyContent (main axis)
|
|
571
|
+
* - horizontalAlignment controls alignItems (cross axis)
|
|
572
|
+
*/
|
|
573
|
+
const justifyContent = isHorizontal
|
|
574
|
+
? mapAlignment(containerLayout.horizontalAlignment)
|
|
575
|
+
: mapAlignment(containerLayout.verticalAlignment);
|
|
576
|
+
|
|
577
|
+
const alignItems = isHorizontal
|
|
578
|
+
? mapAlignment(containerLayout.verticalAlignment)
|
|
579
|
+
: mapAlignment(containerLayout.horizontalAlignment);
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<View style={[styles.stack, style, {
|
|
583
|
+
flexDirection: isHorizontal ? 'row' : 'column',
|
|
584
|
+
gap: gap,
|
|
585
|
+
// justifyContent/alignItems come from mapAlignment whose 'stretch' value
|
|
586
|
+
// is valid for alignItems but not justifyContent; cast (RN ignores it).
|
|
587
|
+
justifyContent,
|
|
588
|
+
alignItems,
|
|
589
|
+
} as ViewStyle]}>
|
|
590
|
+
{component.children?.map((child) => (
|
|
591
|
+
<ComponentRenderer
|
|
592
|
+
key={child.id}
|
|
593
|
+
component={child}
|
|
594
|
+
configuration={configuration}
|
|
595
|
+
theme={theme}
|
|
596
|
+
onButtonPress={onButtonPress}
|
|
597
|
+
stateManager={stateManager}
|
|
598
|
+
isPreviewMode={isPreviewMode}
|
|
599
|
+
/>
|
|
600
|
+
))}
|
|
601
|
+
</View>
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
case 'divider':
|
|
606
|
+
// Use theme border color for divider
|
|
607
|
+
const dividerColor = resolveColor(
|
|
608
|
+
resolvedComponent.appearance?.backgroundColor,
|
|
609
|
+
theme?.borderColor || theme?.resolvedColors?.border || '#E5E7EB'
|
|
610
|
+
);
|
|
611
|
+
return (
|
|
612
|
+
<View
|
|
613
|
+
style={[
|
|
614
|
+
styles.divider,
|
|
615
|
+
style,
|
|
616
|
+
{
|
|
617
|
+
backgroundColor: dividerColor,
|
|
618
|
+
height: processedProperties.orientation === 'vertical' ? '100%' : 1,
|
|
619
|
+
width: processedProperties.orientation === 'vertical' ? 1 : '100%',
|
|
620
|
+
},
|
|
621
|
+
]}
|
|
622
|
+
/>
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
case 'spacer':
|
|
626
|
+
return (
|
|
627
|
+
<View
|
|
628
|
+
style={[
|
|
629
|
+
style,
|
|
630
|
+
{
|
|
631
|
+
width: processedProperties.size || 16,
|
|
632
|
+
height: processedProperties.size || 16,
|
|
633
|
+
},
|
|
634
|
+
]}
|
|
635
|
+
/>
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
// Interactive components (keep dedicated handlers)
|
|
639
|
+
case 'checkbox':
|
|
640
|
+
return (
|
|
641
|
+
<CheckboxComponent
|
|
642
|
+
component={resolvedComponent}
|
|
643
|
+
stateManager={stateManager}
|
|
644
|
+
isPreviewMode={isPreviewMode}
|
|
645
|
+
/>
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
case 'toggle':
|
|
649
|
+
return (
|
|
650
|
+
<ToggleComponent
|
|
651
|
+
component={resolvedComponent}
|
|
652
|
+
stateManager={stateManager}
|
|
653
|
+
isPreviewMode={isPreviewMode}
|
|
654
|
+
/>
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
case 'segmented-picker':
|
|
658
|
+
return (
|
|
659
|
+
<SegmentedPickerComponent
|
|
660
|
+
component={resolvedComponent}
|
|
661
|
+
stateManager={stateManager}
|
|
662
|
+
isPreviewMode={isPreviewMode}
|
|
663
|
+
/>
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
case 'price':
|
|
667
|
+
const priceColor = getTextColor();
|
|
668
|
+
return (
|
|
669
|
+
<View style={[styles.priceContainer, style]}>
|
|
670
|
+
<Text style={[styles.priceText, { color: priceColor }]}>
|
|
671
|
+
{processedProperties.currency || '$'}
|
|
672
|
+
{processedProperties.amount || '0.00'}
|
|
673
|
+
</Text>
|
|
674
|
+
{processedProperties.period && (
|
|
675
|
+
<Text style={[styles.pricePeriod, { color: applyOpacity(priceColor, 0.6) }]}>
|
|
676
|
+
/{processedProperties.period}
|
|
677
|
+
</Text>
|
|
678
|
+
)}
|
|
679
|
+
</View>
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
case 'countdown':
|
|
683
|
+
return (
|
|
684
|
+
<CountdownComponent
|
|
685
|
+
component={resolvedComponent}
|
|
686
|
+
configuration={configuration}
|
|
687
|
+
style={style}
|
|
688
|
+
/>
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
case 'carousel':
|
|
692
|
+
return (
|
|
693
|
+
<CarouselComponent
|
|
694
|
+
component={resolvedComponent}
|
|
695
|
+
configuration={configuration}
|
|
696
|
+
style={style}
|
|
697
|
+
onButtonPress={onButtonPress}
|
|
698
|
+
stateManager={stateManager}
|
|
699
|
+
/>
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
case 'accordion':
|
|
703
|
+
return (
|
|
704
|
+
<AccordionComponent
|
|
705
|
+
component={resolvedComponent}
|
|
706
|
+
renderChild={renderChild}
|
|
707
|
+
stateManager={stateManager}
|
|
708
|
+
isPreviewMode={isPreviewMode}
|
|
709
|
+
/>
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
case 'slider':
|
|
713
|
+
return (
|
|
714
|
+
<SliderComponent
|
|
715
|
+
component={resolvedComponent}
|
|
716
|
+
stateManager={stateManager}
|
|
717
|
+
isPreviewMode={isPreviewMode}
|
|
718
|
+
/>
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
case 'tabs':
|
|
722
|
+
return (
|
|
723
|
+
<TabsComponent
|
|
724
|
+
component={resolvedComponent}
|
|
725
|
+
children={component.children || []}
|
|
726
|
+
renderChild={renderChild}
|
|
727
|
+
stateManager={stateManager}
|
|
728
|
+
isPreviewMode={isPreviewMode}
|
|
729
|
+
/>
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
case 'navigation':
|
|
733
|
+
return (
|
|
734
|
+
<NavigationComponent
|
|
735
|
+
component={resolvedComponent}
|
|
736
|
+
children={component.children || []}
|
|
737
|
+
renderChild={renderChild}
|
|
738
|
+
stateManager={stateManager}
|
|
739
|
+
isPreviewMode={isPreviewMode}
|
|
740
|
+
/>
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
case 'confetti':
|
|
744
|
+
return (
|
|
745
|
+
<ConfettiComponent
|
|
746
|
+
component={resolvedComponent}
|
|
747
|
+
stateManager={stateManager}
|
|
748
|
+
isPreviewMode={isPreviewMode}
|
|
749
|
+
/>
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
case 'delayed-x':
|
|
753
|
+
return (
|
|
754
|
+
<DelayedXComponent
|
|
755
|
+
component={resolvedComponent}
|
|
756
|
+
configuration={configuration}
|
|
757
|
+
renderChild={renderChild}
|
|
758
|
+
stateManager={stateManager}
|
|
759
|
+
isPreviewMode={isPreviewMode}
|
|
760
|
+
/>
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
case 'haptic-feedback':
|
|
764
|
+
return (
|
|
765
|
+
<HapticFeedbackComponent
|
|
766
|
+
component={resolvedComponent}
|
|
767
|
+
isPreviewMode={isPreviewMode}
|
|
768
|
+
/>
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
case 'button': {
|
|
772
|
+
// Theme-aware button rendering
|
|
773
|
+
const buttonBgColor = resolveColor(
|
|
774
|
+
resolvedComponent.appearance?.backgroundColor,
|
|
775
|
+
theme?.primaryColor || theme?.resolvedColors?.primary || '#007AFF'
|
|
776
|
+
);
|
|
777
|
+
const buttonTextColor = processedProperties.textColor
|
|
778
|
+
? resolveColor(processedProperties.textColor)
|
|
779
|
+
: '#FFFFFF';
|
|
780
|
+
|
|
781
|
+
return (
|
|
782
|
+
<TouchableOpacity
|
|
783
|
+
style={[styles.button, style, { backgroundColor: buttonBgColor }]}
|
|
784
|
+
onPress={() => onButtonPress?.(component.id, processedProperties.action || 'tap')}
|
|
785
|
+
>
|
|
786
|
+
<Text style={[styles.buttonText, { color: buttonTextColor }]}>
|
|
787
|
+
{resolveVariable(processedProperties.text || processedProperties.label || 'Button')}
|
|
788
|
+
</Text>
|
|
789
|
+
</TouchableOpacity>
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
case 'badge': {
|
|
794
|
+
const badgeBgColor = resolveColor(
|
|
795
|
+
resolvedComponent.appearance?.backgroundColor,
|
|
796
|
+
theme?.primaryColor || '#FF3B30'
|
|
797
|
+
);
|
|
798
|
+
const badgeTextColor = processedProperties.textColor
|
|
799
|
+
? resolveColor(processedProperties.textColor)
|
|
800
|
+
: '#FFFFFF';
|
|
801
|
+
|
|
802
|
+
return (
|
|
803
|
+
<View style={[styles.badge, style, { backgroundColor: badgeBgColor }]}>
|
|
804
|
+
<Text style={[styles.badgeText, { color: badgeTextColor }]}>
|
|
805
|
+
{resolveVariable(processedProperties.text || '')}
|
|
806
|
+
</Text>
|
|
807
|
+
</View>
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
case 'feature-list': {
|
|
812
|
+
const features = processedProperties.features || [];
|
|
813
|
+
const checkColor = theme?.primaryColor || theme?.resolvedColors?.primary || '#34C759';
|
|
814
|
+
const textColor = getTextColor();
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<View style={[styles.featureList, style]}>
|
|
818
|
+
{features.map((feature: string, index: number) => (
|
|
819
|
+
<View key={index} style={styles.featureItem}>
|
|
820
|
+
<Text style={[styles.featureIcon, { color: checkColor }]}>✓</Text>
|
|
821
|
+
<Text style={[styles.featureText, { color: textColor }]}>
|
|
822
|
+
{resolveVariable(feature)}
|
|
823
|
+
</Text>
|
|
824
|
+
</View>
|
|
825
|
+
))}
|
|
826
|
+
</View>
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
case 'shape': {
|
|
832
|
+
// Render shape primitives (rectangles, circles, etc.)
|
|
833
|
+
const shapeType = processedProperties.shapeType || 'rectangle';
|
|
834
|
+
const cornerRadius = resolvedComponent.appearance?.cornerRadius || 0;
|
|
835
|
+
const backgroundColor = resolvedComponent.appearance?.backgroundColor;
|
|
836
|
+
const borderWidth = resolvedComponent.appearance?.borderWidth || 0;
|
|
837
|
+
const borderColor = resolvedComponent.appearance?.borderColor;
|
|
838
|
+
// backgroundColor may be a plain color string or a structured object;
|
|
839
|
+
// only the object branch carries an opacity field.
|
|
840
|
+
const opacity = (typeof backgroundColor === 'object' ? backgroundColor?.opacity : undefined) ?? 1;
|
|
841
|
+
|
|
842
|
+
// Check for gradient background
|
|
843
|
+
const gradientConfig = extractGradientConfig(backgroundColor);
|
|
844
|
+
const effectiveBorderRadius = shapeType === 'circle' ? 9999 : cornerRadius;
|
|
845
|
+
|
|
846
|
+
// If gradient is detected and supported, use GradientView
|
|
847
|
+
if (gradientConfig && isGradientSupported()) {
|
|
848
|
+
return (
|
|
849
|
+
<GradientView
|
|
850
|
+
gradient={gradientConfig}
|
|
851
|
+
fallbackColor={parseColorValue(backgroundColor)}
|
|
852
|
+
style={[style, { opacity }]}
|
|
853
|
+
borderRadius={effectiveBorderRadius}
|
|
854
|
+
borderWidth={borderWidth}
|
|
855
|
+
borderColor={parseColorValue(borderColor)}
|
|
856
|
+
/>
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Fallback to solid color (uses first stop color for gradients)
|
|
861
|
+
const bgColor = parseColorValue(backgroundColor);
|
|
862
|
+
|
|
863
|
+
return (
|
|
864
|
+
<View
|
|
865
|
+
style={[
|
|
866
|
+
style,
|
|
867
|
+
{
|
|
868
|
+
backgroundColor: bgColor,
|
|
869
|
+
borderRadius: effectiveBorderRadius,
|
|
870
|
+
borderWidth,
|
|
871
|
+
borderColor: parseColorValue(borderColor),
|
|
872
|
+
opacity,
|
|
873
|
+
}
|
|
874
|
+
]}
|
|
875
|
+
/>
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
case 'icon': {
|
|
880
|
+
// Render icon primitives using Lucide icons (same as web editor)
|
|
881
|
+
const rawIcon = processedProperties.iconName;
|
|
882
|
+
let iconName = 'Circle';
|
|
883
|
+
const { Platform } = require('react-native');
|
|
884
|
+
|
|
885
|
+
if (rawIcon && typeof rawIcon === 'object' && 'iconName' in rawIcon) {
|
|
886
|
+
if (Platform.OS === 'ios' && (rawIcon as any).iconNameIos) {
|
|
887
|
+
iconName = (rawIcon as any).iconNameIos;
|
|
888
|
+
} else if (Platform.OS === 'android' && (rawIcon as any).iconNameAndroid) {
|
|
889
|
+
iconName = (rawIcon as any).iconNameAndroid;
|
|
890
|
+
} else {
|
|
891
|
+
iconName = (rawIcon as any).iconName || 'Circle';
|
|
892
|
+
}
|
|
893
|
+
} else if (typeof rawIcon === 'string') {
|
|
894
|
+
if (Platform.OS === 'ios' && processedProperties.iconNameIos) {
|
|
895
|
+
iconName = processedProperties.iconNameIos;
|
|
896
|
+
} else if (Platform.OS === 'android' && processedProperties.iconNameAndroid) {
|
|
897
|
+
iconName = processedProperties.iconNameAndroid;
|
|
898
|
+
} else {
|
|
899
|
+
iconName = rawIcon;
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
iconName = (processedProperties.name as string) || 'Circle';
|
|
903
|
+
}
|
|
904
|
+
const iconSize = parseSizeValue(processedProperties.iconSize) || parseSizeValue(processedProperties.size) || 24;
|
|
905
|
+
const iconColor = resolveColor(
|
|
906
|
+
processedProperties.iconColor || processedProperties.color,
|
|
907
|
+
getTextColor()
|
|
908
|
+
);
|
|
909
|
+
const strokeWidth = typeof processedProperties.strokeWidth === 'number'
|
|
910
|
+
? processedProperties.strokeWidth
|
|
911
|
+
: 2;
|
|
912
|
+
|
|
913
|
+
// Use getLucideIcon for SVG icon parity with web editor
|
|
914
|
+
const { getLucideIcon } = require('./utils/iconMapper');
|
|
915
|
+
const { component: IconComp, fallback } = getLucideIcon(iconName);
|
|
916
|
+
|
|
917
|
+
if (IconComp) {
|
|
918
|
+
return (
|
|
919
|
+
<View style={[style, { alignItems: 'center', justifyContent: 'center' }]}>
|
|
920
|
+
<IconComp size={iconSize} color={iconColor} strokeWidth={strokeWidth} />
|
|
921
|
+
</View>
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Emoji fallback for unmapped icons
|
|
926
|
+
const fallbackEmoji: Record<string, string> = {
|
|
927
|
+
'check': '✓', 'x': '✕', 'plus': '+', 'minus': '−',
|
|
928
|
+
'star': '☆', 'heart': '♡', 'circle': '○',
|
|
929
|
+
};
|
|
930
|
+
const emojiChar = fallbackEmoji[iconName.toLowerCase()] || '○';
|
|
931
|
+
|
|
932
|
+
return (
|
|
933
|
+
<View style={[style, { width: iconSize, height: iconSize, alignItems: 'center', justifyContent: 'center' }]}>
|
|
934
|
+
<Text style={{ fontSize: iconSize * 0.8, color: iconColor, lineHeight: iconSize }}>
|
|
935
|
+
{emojiChar}
|
|
936
|
+
</Text>
|
|
937
|
+
</View>
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
case 'webp':
|
|
942
|
+
case 'gif': {
|
|
943
|
+
// Render animated/static images — same property chain as 'image'
|
|
944
|
+
const mediaUrl = processedProperties.imageUrl
|
|
945
|
+
|| processedProperties.src
|
|
946
|
+
|| processedProperties.source
|
|
947
|
+
|| '';
|
|
948
|
+
const mediaObjectFit = processedProperties.objectFit || 'cover';
|
|
949
|
+
const mediaAlt = processedProperties.altText || processedProperties.alt || '';
|
|
950
|
+
|
|
951
|
+
return mediaUrl ? (
|
|
952
|
+
<Image
|
|
953
|
+
source={{ uri: mediaUrl }}
|
|
954
|
+
// See note above: cast shared ViewStyle to ImageStyle for Image.
|
|
955
|
+
style={[styles.image, style as ImageStyle]}
|
|
956
|
+
resizeMode={mediaObjectFit as 'cover' | 'contain' | 'stretch' | 'center'}
|
|
957
|
+
accessibilityLabel={mediaAlt}
|
|
958
|
+
/>
|
|
959
|
+
) : (
|
|
960
|
+
<View style={[styles.mediaPlaceholder, style]}>
|
|
961
|
+
<Text style={styles.placeholderIcon}>🖼</Text>
|
|
962
|
+
<Text style={styles.placeholderText}>{component.type.toUpperCase()}</Text>
|
|
963
|
+
</View>
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
case 'video': {
|
|
968
|
+
// Video placeholder - in production, use expo-av or react-native-video
|
|
969
|
+
return (
|
|
970
|
+
<View style={[styles.mediaPlaceholder, style]}>
|
|
971
|
+
<Text style={styles.placeholderIcon}>🎬</Text>
|
|
972
|
+
<Text style={styles.placeholderText}>Video</Text>
|
|
973
|
+
{processedProperties.source && (
|
|
974
|
+
<Text style={styles.placeholderHint}>Source: {processedProperties.source}</Text>
|
|
975
|
+
)}
|
|
976
|
+
</View>
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
case 'lottie': {
|
|
981
|
+
// Lottie animation placeholder - in production, use lottie-react-native
|
|
982
|
+
return (
|
|
983
|
+
<View style={[styles.mediaPlaceholder, style]}>
|
|
984
|
+
<Text style={styles.placeholderIcon}>✨</Text>
|
|
985
|
+
<Text style={styles.placeholderText}>Lottie Animation</Text>
|
|
986
|
+
{processedProperties.source && (
|
|
987
|
+
<Text style={styles.placeholderHint}>Source: {processedProperties.source}</Text>
|
|
988
|
+
)}
|
|
989
|
+
</View>
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
case 'progress-loader':
|
|
994
|
+
return (
|
|
995
|
+
<ProgressLoaderComponent
|
|
996
|
+
component={resolvedComponent}
|
|
997
|
+
stateManager={stateManager}
|
|
998
|
+
isPreviewMode={isPreviewMode}
|
|
999
|
+
/>
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
case 'chart-curve':
|
|
1003
|
+
return (
|
|
1004
|
+
<ChartCurveComponent
|
|
1005
|
+
component={resolvedComponent}
|
|
1006
|
+
stateManager={stateManager}
|
|
1007
|
+
isPreviewMode={isPreviewMode}
|
|
1008
|
+
/>
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
case 'single-choice-grid':
|
|
1012
|
+
return (
|
|
1013
|
+
<SingleChoiceGridComponent
|
|
1014
|
+
component={resolvedComponent}
|
|
1015
|
+
stateManager={stateManager}
|
|
1016
|
+
isPreviewMode={isPreviewMode}
|
|
1017
|
+
/>
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
case 'multi-choice-grid':
|
|
1021
|
+
return (
|
|
1022
|
+
<MultiChoiceGridComponent
|
|
1023
|
+
component={resolvedComponent}
|
|
1024
|
+
stateManager={stateManager}
|
|
1025
|
+
isPreviewMode={isPreviewMode}
|
|
1026
|
+
/>
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
case 'drawer-modal':
|
|
1030
|
+
return (
|
|
1031
|
+
<DrawerModalComponent
|
|
1032
|
+
component={resolvedComponent}
|
|
1033
|
+
stateManager={stateManager}
|
|
1034
|
+
isPreviewMode={isPreviewMode}
|
|
1035
|
+
renderComponent={renderChild}
|
|
1036
|
+
/>
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
case 'timeline':
|
|
1040
|
+
return (
|
|
1041
|
+
<TimelineComponent
|
|
1042
|
+
component={resolvedComponent}
|
|
1043
|
+
stateManager={stateManager}
|
|
1044
|
+
isPreviewMode={isPreviewMode}
|
|
1045
|
+
renderComponent={renderChild}
|
|
1046
|
+
/>
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
case 'text-input':
|
|
1050
|
+
return (
|
|
1051
|
+
<TextInputComponent
|
|
1052
|
+
component={resolvedComponent}
|
|
1053
|
+
stateManager={stateManager}
|
|
1054
|
+
isPreviewMode={isPreviewMode}
|
|
1055
|
+
/>
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
case 'star-rating-input':
|
|
1059
|
+
return (
|
|
1060
|
+
<StarRatingInputComponent
|
|
1061
|
+
component={resolvedComponent}
|
|
1062
|
+
stateManager={stateManager}
|
|
1063
|
+
isPreviewMode={isPreviewMode}
|
|
1064
|
+
/>
|
|
1065
|
+
);
|
|
1066
|
+
|
|
1067
|
+
case 'nps-scale':
|
|
1068
|
+
return (
|
|
1069
|
+
<NPSScaleComponent
|
|
1070
|
+
component={resolvedComponent}
|
|
1071
|
+
stateManager={stateManager}
|
|
1072
|
+
isPreviewMode={isPreviewMode}
|
|
1073
|
+
/>
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
case 'oauth-button':
|
|
1077
|
+
return (
|
|
1078
|
+
<OAuthButtonComponent
|
|
1079
|
+
component={resolvedComponent}
|
|
1080
|
+
stateManager={stateManager}
|
|
1081
|
+
isPreviewMode={isPreviewMode}
|
|
1082
|
+
/>
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
case 'progress-stepper':
|
|
1086
|
+
return (
|
|
1087
|
+
<ProgressStepperComponent
|
|
1088
|
+
component={resolvedComponent}
|
|
1089
|
+
stateManager={stateManager}
|
|
1090
|
+
isPreviewMode={isPreviewMode}
|
|
1091
|
+
/>
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
case 'share-code':
|
|
1095
|
+
return (
|
|
1096
|
+
<ShareCodeComponent
|
|
1097
|
+
component={resolvedComponent}
|
|
1098
|
+
stateManager={stateManager}
|
|
1099
|
+
isPreviewMode={isPreviewMode}
|
|
1100
|
+
/>
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
case 'share-buttons':
|
|
1104
|
+
return (
|
|
1105
|
+
<ShareButtonsComponent
|
|
1106
|
+
component={resolvedComponent}
|
|
1107
|
+
stateManager={stateManager}
|
|
1108
|
+
isPreviewMode={isPreviewMode}
|
|
1109
|
+
renderChild={(child, index) => (
|
|
1110
|
+
<ComponentRenderer
|
|
1111
|
+
key={child.id}
|
|
1112
|
+
component={child}
|
|
1113
|
+
configuration={configuration}
|
|
1114
|
+
theme={theme}
|
|
1115
|
+
onButtonPress={onButtonPress}
|
|
1116
|
+
stateManager={stateManager}
|
|
1117
|
+
isPreviewMode={isPreviewMode}
|
|
1118
|
+
/>
|
|
1119
|
+
)}
|
|
1120
|
+
/>
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
case 'vertical-products':
|
|
1124
|
+
return (
|
|
1125
|
+
<VerticalProductsComponent
|
|
1126
|
+
component={resolvedComponent}
|
|
1127
|
+
configuration={configuration}
|
|
1128
|
+
renderChild={(child: PaywallComponent) => (
|
|
1129
|
+
<ComponentRenderer
|
|
1130
|
+
key={child.id}
|
|
1131
|
+
component={child}
|
|
1132
|
+
configuration={configuration}
|
|
1133
|
+
theme={theme}
|
|
1134
|
+
onButtonPress={onButtonPress}
|
|
1135
|
+
stateManager={stateManager}
|
|
1136
|
+
isPreviewMode={isPreviewMode}
|
|
1137
|
+
/>
|
|
1138
|
+
)}
|
|
1139
|
+
stateManager={stateManager}
|
|
1140
|
+
onAction={onButtonPress}
|
|
1141
|
+
/>
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
case 'horizontal-products':
|
|
1145
|
+
return (
|
|
1146
|
+
<HorizontalProductsComponent
|
|
1147
|
+
component={resolvedComponent}
|
|
1148
|
+
configuration={configuration}
|
|
1149
|
+
renderChild={(child: PaywallComponent) => (
|
|
1150
|
+
<ComponentRenderer
|
|
1151
|
+
key={child.id}
|
|
1152
|
+
component={child}
|
|
1153
|
+
configuration={configuration}
|
|
1154
|
+
theme={theme}
|
|
1155
|
+
onButtonPress={onButtonPress}
|
|
1156
|
+
stateManager={stateManager}
|
|
1157
|
+
isPreviewMode={isPreviewMode}
|
|
1158
|
+
/>
|
|
1159
|
+
)}
|
|
1160
|
+
stateManager={stateManager}
|
|
1161
|
+
onAction={onButtonPress}
|
|
1162
|
+
/>
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
case 'accolade-cps':
|
|
1166
|
+
return (
|
|
1167
|
+
<AccoladeCpsComponent
|
|
1168
|
+
component={resolvedComponent}
|
|
1169
|
+
stateManager={stateManager}
|
|
1170
|
+
isPreviewMode={isPreviewMode}
|
|
1171
|
+
/>
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
case 'sticker-composite':
|
|
1175
|
+
return (
|
|
1176
|
+
<StickerCompositeComponent
|
|
1177
|
+
component={resolvedComponent}
|
|
1178
|
+
stateManager={stateManager}
|
|
1179
|
+
isPreviewMode={isPreviewMode}
|
|
1180
|
+
/>
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
default:
|
|
1184
|
+
// Fallback for unknown component types - render children if available (composite handling)
|
|
1185
|
+
if (component.children?.length) {
|
|
1186
|
+
return renderWithChildren(component);
|
|
1187
|
+
}
|
|
1188
|
+
return <View style={[styles.fallback, style]} />;
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* StyleSheet with minimal defaults
|
|
1194
|
+
* Colors should come from theme, not hardcoded here
|
|
1195
|
+
*/
|
|
1196
|
+
const styles = StyleSheet.create({
|
|
1197
|
+
text: {
|
|
1198
|
+
fontSize: 16,
|
|
1199
|
+
// Color set dynamically from theme
|
|
1200
|
+
},
|
|
1201
|
+
image: {
|
|
1202
|
+
width: 100,
|
|
1203
|
+
height: 100,
|
|
1204
|
+
},
|
|
1205
|
+
button: {
|
|
1206
|
+
paddingVertical: 12,
|
|
1207
|
+
paddingHorizontal: 24,
|
|
1208
|
+
borderRadius: 8,
|
|
1209
|
+
alignItems: 'center',
|
|
1210
|
+
justifyContent: 'center',
|
|
1211
|
+
},
|
|
1212
|
+
buttonText: {
|
|
1213
|
+
fontSize: 16,
|
|
1214
|
+
fontWeight: '600',
|
|
1215
|
+
},
|
|
1216
|
+
stack: {
|
|
1217
|
+
flexDirection: 'column',
|
|
1218
|
+
},
|
|
1219
|
+
divider: {
|
|
1220
|
+
// backgroundColor set dynamically from theme
|
|
1221
|
+
},
|
|
1222
|
+
badge: {
|
|
1223
|
+
paddingVertical: 4,
|
|
1224
|
+
paddingHorizontal: 8,
|
|
1225
|
+
borderRadius: 4,
|
|
1226
|
+
},
|
|
1227
|
+
badgeText: {
|
|
1228
|
+
fontSize: 12,
|
|
1229
|
+
fontWeight: '600',
|
|
1230
|
+
},
|
|
1231
|
+
featureList: {
|
|
1232
|
+
gap: 8,
|
|
1233
|
+
},
|
|
1234
|
+
featureItem: {
|
|
1235
|
+
flexDirection: 'row',
|
|
1236
|
+
alignItems: 'center',
|
|
1237
|
+
gap: 8,
|
|
1238
|
+
},
|
|
1239
|
+
featureIcon: {
|
|
1240
|
+
fontSize: 16,
|
|
1241
|
+
fontWeight: 'bold',
|
|
1242
|
+
},
|
|
1243
|
+
featureText: {
|
|
1244
|
+
fontSize: 14,
|
|
1245
|
+
},
|
|
1246
|
+
priceContainer: {
|
|
1247
|
+
flexDirection: 'row',
|
|
1248
|
+
alignItems: 'baseline',
|
|
1249
|
+
},
|
|
1250
|
+
priceText: {
|
|
1251
|
+
fontSize: 32,
|
|
1252
|
+
fontWeight: 'bold',
|
|
1253
|
+
},
|
|
1254
|
+
pricePeriod: {
|
|
1255
|
+
fontSize: 16,
|
|
1256
|
+
marginLeft: 4,
|
|
1257
|
+
},
|
|
1258
|
+
package: {
|
|
1259
|
+
padding: 16,
|
|
1260
|
+
borderRadius: 12,
|
|
1261
|
+
borderWidth: 2,
|
|
1262
|
+
},
|
|
1263
|
+
packageTitle: {
|
|
1264
|
+
fontSize: 18,
|
|
1265
|
+
fontWeight: '600',
|
|
1266
|
+
},
|
|
1267
|
+
packageSubtitle: {
|
|
1268
|
+
fontSize: 14,
|
|
1269
|
+
marginTop: 4,
|
|
1270
|
+
},
|
|
1271
|
+
fallback: {
|
|
1272
|
+
padding: 8,
|
|
1273
|
+
},
|
|
1274
|
+
advancedWarning: {
|
|
1275
|
+
backgroundColor: '#FFF3CD',
|
|
1276
|
+
padding: 16,
|
|
1277
|
+
borderRadius: 12,
|
|
1278
|
+
borderWidth: 1,
|
|
1279
|
+
borderColor: '#FFE69C',
|
|
1280
|
+
alignItems: 'center',
|
|
1281
|
+
},
|
|
1282
|
+
warningIcon: {
|
|
1283
|
+
fontSize: 32,
|
|
1284
|
+
marginBottom: 8,
|
|
1285
|
+
},
|
|
1286
|
+
warningTitle: {
|
|
1287
|
+
fontSize: 16,
|
|
1288
|
+
fontWeight: '600',
|
|
1289
|
+
color: '#000000',
|
|
1290
|
+
marginBottom: 4,
|
|
1291
|
+
},
|
|
1292
|
+
warningText: {
|
|
1293
|
+
fontSize: 13,
|
|
1294
|
+
color: '#856404',
|
|
1295
|
+
textAlign: 'center',
|
|
1296
|
+
marginBottom: 4,
|
|
1297
|
+
},
|
|
1298
|
+
warningHint: {
|
|
1299
|
+
fontSize: 11,
|
|
1300
|
+
color: '#0056B3',
|
|
1301
|
+
textAlign: 'center',
|
|
1302
|
+
},
|
|
1303
|
+
mediaPlaceholder: {
|
|
1304
|
+
backgroundColor: '#F0F0F0',
|
|
1305
|
+
padding: 16,
|
|
1306
|
+
borderRadius: 8,
|
|
1307
|
+
alignItems: 'center',
|
|
1308
|
+
justifyContent: 'center',
|
|
1309
|
+
minHeight: 60,
|
|
1310
|
+
},
|
|
1311
|
+
placeholderIcon: {
|
|
1312
|
+
fontSize: 24,
|
|
1313
|
+
marginBottom: 4,
|
|
1314
|
+
},
|
|
1315
|
+
placeholderText: {
|
|
1316
|
+
fontSize: 12,
|
|
1317
|
+
fontWeight: '600',
|
|
1318
|
+
color: '#666666',
|
|
1319
|
+
},
|
|
1320
|
+
placeholderHint: {
|
|
1321
|
+
fontSize: 10,
|
|
1322
|
+
color: '#999999',
|
|
1323
|
+
marginTop: 4,
|
|
1324
|
+
},
|
|
1325
|
+
});
|