@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.
Files changed (309) hide show
  1. package/BillDogReactNative.podspec +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/android/build.gradle +99 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/kotlin/io/billdog/reactnative/BillDogABTestModule.kt +317 -0
  7. package/android/src/main/kotlin/io/billdog/reactnative/BillDogAnalyticsModule.kt +389 -0
  8. package/android/src/main/kotlin/io/billdog/reactnative/BillDogModule.kt +817 -0
  9. package/android/src/main/kotlin/io/billdog/reactnative/BillDogPaywallViewManager.kt +383 -0
  10. package/android/src/main/kotlin/io/billdog/reactnative/BillDogSurveyModule.kt +427 -0
  11. package/android/src/main/kotlin/io/billdog/reactnative/BillDogVirtualCurrencyModule.kt +407 -0
  12. package/android/src/main/kotlin/io/billdog/virtualcurrency/BillDogVirtualCurrencyPackage.kt +44 -0
  13. package/dist/AdvancedPaywallCache.d.ts +70 -0
  14. package/dist/AdvancedPaywallCache.js +344 -0
  15. package/dist/BillDog.d.ts +255 -0
  16. package/dist/BillDog.js +918 -0
  17. package/dist/BillDogConfiguration.d.ts +145 -0
  18. package/dist/BillDogConfiguration.js +229 -0
  19. package/dist/ComponentRenderer.d.ts +17 -0
  20. package/dist/ComponentRenderer.js +886 -0
  21. package/dist/LayoutEngine.d.ts +101 -0
  22. package/dist/LayoutEngine.js +394 -0
  23. package/dist/LocalizationResolver.d.ts +99 -0
  24. package/dist/LocalizationResolver.js +266 -0
  25. package/dist/NativePaywallView.d.ts +36 -0
  26. package/dist/NativePaywallView.js +59 -0
  27. package/dist/PaywallCache.d.ts +74 -0
  28. package/dist/PaywallCache.js +293 -0
  29. package/dist/PaywallStateManager.d.ts +62 -0
  30. package/dist/PaywallStateManager.js +147 -0
  31. package/dist/PaywallView.d.ts +42 -0
  32. package/dist/PaywallView.js +275 -0
  33. package/dist/ProductVariableProvider.d.ts +60 -0
  34. package/dist/ProductVariableProvider.js +239 -0
  35. package/dist/VariableResolver.d.ts +120 -0
  36. package/dist/VariableResolver.js +379 -0
  37. package/dist/WebViewPaywall.d.ts +75 -0
  38. package/dist/WebViewPaywall.js +360 -0
  39. package/dist/animations/presets.d.ts +49 -0
  40. package/dist/animations/presets.js +367 -0
  41. package/dist/components/AccoladeCpsComponent.d.ts +13 -0
  42. package/dist/components/AccoladeCpsComponent.js +248 -0
  43. package/dist/components/AccordionComponent.d.ts +10 -0
  44. package/dist/components/AccordionComponent.js +187 -0
  45. package/dist/components/AnimatedWrapper.d.ts +35 -0
  46. package/dist/components/AnimatedWrapper.js +277 -0
  47. package/dist/components/CarouselComponent.d.ts +23 -0
  48. package/dist/components/CarouselComponent.js +484 -0
  49. package/dist/components/ChartCurveComponent.d.ts +13 -0
  50. package/dist/components/ChartCurveComponent.js +333 -0
  51. package/dist/components/CheckboxComponent.d.ts +9 -0
  52. package/dist/components/CheckboxComponent.js +172 -0
  53. package/dist/components/ConfettiComponent.d.ts +9 -0
  54. package/dist/components/ConfettiComponent.js +172 -0
  55. package/dist/components/CountdownComponent.d.ts +16 -0
  56. package/dist/components/CountdownComponent.js +414 -0
  57. package/dist/components/DelayedXComponent.d.ts +12 -0
  58. package/dist/components/DelayedXComponent.js +240 -0
  59. package/dist/components/DrawerModalComponent.d.ts +20 -0
  60. package/dist/components/DrawerModalComponent.js +286 -0
  61. package/dist/components/GradientView.d.ts +38 -0
  62. package/dist/components/GradientView.js +126 -0
  63. package/dist/components/HapticFeedbackComponent.d.ts +16 -0
  64. package/dist/components/HapticFeedbackComponent.js +64 -0
  65. package/dist/components/HorizontalProductsComponent.d.ts +17 -0
  66. package/dist/components/HorizontalProductsComponent.js +284 -0
  67. package/dist/components/MultiChoiceGridComponent.d.ts +13 -0
  68. package/dist/components/MultiChoiceGridComponent.js +234 -0
  69. package/dist/components/NPSScaleComponent.d.ts +16 -0
  70. package/dist/components/NPSScaleComponent.js +260 -0
  71. package/dist/components/NavigationComponent.d.ts +26 -0
  72. package/dist/components/NavigationComponent.js +302 -0
  73. package/dist/components/OAuthButtonComponent.d.ts +20 -0
  74. package/dist/components/OAuthButtonComponent.js +237 -0
  75. package/dist/components/ProgressLoaderComponent.d.ts +13 -0
  76. package/dist/components/ProgressLoaderComponent.js +308 -0
  77. package/dist/components/ProgressStepperComponent.d.ts +18 -0
  78. package/dist/components/ProgressStepperComponent.js +228 -0
  79. package/dist/components/SegmentedPickerComponent.d.ts +10 -0
  80. package/dist/components/SegmentedPickerComponent.js +221 -0
  81. package/dist/components/ShareButtonsComponent.d.ts +21 -0
  82. package/dist/components/ShareButtonsComponent.js +265 -0
  83. package/dist/components/ShareCodeComponent.d.ts +16 -0
  84. package/dist/components/ShareCodeComponent.js +274 -0
  85. package/dist/components/SingleChoiceGridComponent.d.ts +13 -0
  86. package/dist/components/SingleChoiceGridComponent.js +210 -0
  87. package/dist/components/SliderComponent.d.ts +9 -0
  88. package/dist/components/SliderComponent.js +263 -0
  89. package/dist/components/StarRatingInputComponent.d.ts +20 -0
  90. package/dist/components/StarRatingInputComponent.js +198 -0
  91. package/dist/components/StickerCompositeComponent.d.ts +13 -0
  92. package/dist/components/StickerCompositeComponent.js +136 -0
  93. package/dist/components/TabsComponent.d.ts +19 -0
  94. package/dist/components/TabsComponent.js +431 -0
  95. package/dist/components/TextInputComponent.d.ts +18 -0
  96. package/dist/components/TextInputComponent.js +227 -0
  97. package/dist/components/TimelineComponent.d.ts +19 -0
  98. package/dist/components/TimelineComponent.js +104 -0
  99. package/dist/components/ToggleComponent.d.ts +14 -0
  100. package/dist/components/ToggleComponent.js +132 -0
  101. package/dist/components/VerticalProductsComponent.d.ts +17 -0
  102. package/dist/components/VerticalProductsComponent.js +311 -0
  103. package/dist/dynamicValues.d.ts +36 -0
  104. package/dist/dynamicValues.js +142 -0
  105. package/dist/filters/dateFilters.d.ts +50 -0
  106. package/dist/filters/dateFilters.js +390 -0
  107. package/dist/filters/index.d.ts +18 -0
  108. package/dist/filters/index.js +253 -0
  109. package/dist/hooks/useOfferings.d.ts +76 -0
  110. package/dist/hooks/useOfferings.js +99 -0
  111. package/dist/hooks/usePaywall.d.ts +27 -0
  112. package/dist/hooks/usePaywall.js +114 -0
  113. package/dist/hooks/usePlacement.d.ts +34 -0
  114. package/dist/hooks/usePlacement.js +89 -0
  115. package/dist/hooks/useSubscription.d.ts +40 -0
  116. package/dist/hooks/useSubscription.js +97 -0
  117. package/dist/index.d.ts +98 -0
  118. package/dist/index.js +198 -0
  119. package/dist/models/BillDogError.d.ts +40 -0
  120. package/dist/models/BillDogError.js +11 -0
  121. package/dist/models/CacheFetchPolicy.d.ts +8 -0
  122. package/dist/models/CacheFetchPolicy.js +4 -0
  123. package/dist/models/CustomProduct.d.ts +32 -0
  124. package/dist/models/CustomProduct.js +15 -0
  125. package/dist/models/CustomerInfo.d.ts +17 -0
  126. package/dist/models/CustomerInfo.js +11 -0
  127. package/dist/models/Delegate.d.ts +20 -0
  128. package/dist/models/Delegate.js +11 -0
  129. package/dist/models/EntitlementInfo.d.ts +19 -0
  130. package/dist/models/EntitlementInfo.js +2 -0
  131. package/dist/models/Placement.d.ts +54 -0
  132. package/dist/models/Placement.js +15 -0
  133. package/dist/models/PurchaseParams.d.ts +39 -0
  134. package/dist/models/PurchaseParams.js +60 -0
  135. package/dist/models/PurchaseResult.d.ts +30 -0
  136. package/dist/models/PurchaseResult.js +15 -0
  137. package/dist/models/RestoreResult.d.ts +30 -0
  138. package/dist/models/RestoreResult.js +22 -0
  139. package/dist/models/RestoredItem.d.ts +26 -0
  140. package/dist/models/RestoredItem.js +13 -0
  141. package/dist/modules/ABTest.d.ts +65 -0
  142. package/dist/modules/ABTest.js +113 -0
  143. package/dist/modules/Analytics.d.ts +91 -0
  144. package/dist/modules/Analytics.js +182 -0
  145. package/dist/modules/Survey.d.ts +65 -0
  146. package/dist/modules/Survey.js +82 -0
  147. package/dist/modules/VirtualCurrency.d.ts +63 -0
  148. package/dist/modules/VirtualCurrency.js +147 -0
  149. package/dist/murmur.d.ts +23 -0
  150. package/dist/murmur.js +104 -0
  151. package/dist/notifications/LiveActivitiesManager.d.ts +47 -0
  152. package/dist/notifications/LiveActivitiesManager.js +106 -0
  153. package/dist/notifications/NotificationScheduler.d.ts +55 -0
  154. package/dist/notifications/NotificationScheduler.js +199 -0
  155. package/dist/notifications/PurchaseNotificationHandler.d.ts +24 -0
  156. package/dist/notifications/PurchaseNotificationHandler.js +55 -0
  157. package/dist/notifications/useDeviceToken.d.ts +36 -0
  158. package/dist/notifications/useDeviceToken.js +97 -0
  159. package/dist/notifications/useLiveActivity.d.ts +39 -0
  160. package/dist/notifications/useLiveActivity.js +104 -0
  161. package/dist/review/ReviewAnalytics.d.ts +34 -0
  162. package/dist/review/ReviewAnalytics.js +73 -0
  163. package/dist/review/ReviewManager.d.ts +83 -0
  164. package/dist/review/ReviewManager.js +339 -0
  165. package/dist/review/ReviewTimingManager.d.ts +82 -0
  166. package/dist/review/ReviewTimingManager.js +224 -0
  167. package/dist/survey-core/types.d.ts +215 -0
  168. package/dist/survey-core/types.js +8 -0
  169. package/dist/types.d.ts +438 -0
  170. package/dist/types.js +158 -0
  171. package/dist/utils/ApiClient.d.ts +65 -0
  172. package/dist/utils/ApiClient.js +132 -0
  173. package/dist/utils/CSSConverter.d.ts +49 -0
  174. package/dist/utils/CSSConverter.js +470 -0
  175. package/dist/utils/animations.d.ts +41 -0
  176. package/dist/utils/animations.js +194 -0
  177. package/dist/utils/calendarIntegration.d.ts +36 -0
  178. package/dist/utils/calendarIntegration.js +89 -0
  179. package/dist/utils/configurationResolver.d.ts +23 -0
  180. package/dist/utils/configurationResolver.js +78 -0
  181. package/dist/utils/deviceDetection.d.ts +38 -0
  182. package/dist/utils/deviceDetection.js +59 -0
  183. package/dist/utils/hapticFeedback.d.ts +17 -0
  184. package/dist/utils/hapticFeedback.js +53 -0
  185. package/dist/utils/iconMapper.d.ts +26 -0
  186. package/dist/utils/iconMapper.js +113 -0
  187. package/dist/utils/notificationPermissions.d.ts +13 -0
  188. package/dist/utils/notificationPermissions.js +51 -0
  189. package/dist/utils/priceFormatting.d.ts +19 -0
  190. package/dist/utils/priceFormatting.js +99 -0
  191. package/dist/utils/shareSheet.d.ts +17 -0
  192. package/dist/utils/shareSheet.js +42 -0
  193. package/dist/utils/subscriptionUpgrade.d.ts +84 -0
  194. package/dist/utils/subscriptionUpgrade.js +100 -0
  195. package/dist/valueUtils.d.ts +48 -0
  196. package/dist/valueUtils.js +129 -0
  197. package/dist/xFlowTags.d.ts +18 -0
  198. package/dist/xFlowTags.js +385 -0
  199. package/ios/BillDogABTest.m +50 -0
  200. package/ios/BillDogABTest.swift +188 -0
  201. package/ios/BillDogAnalytics.m +105 -0
  202. package/ios/BillDogAnalytics.swift +324 -0
  203. package/ios/BillDogBridge.m +222 -0
  204. package/ios/BillDogBridge.swift +715 -0
  205. package/ios/BillDogLiveActivities.m +41 -0
  206. package/ios/BillDogLiveActivities.swift +104 -0
  207. package/ios/BillDogPaywallViewManager.m +39 -0
  208. package/ios/BillDogPaywallViewManager.swift +361 -0
  209. package/ios/BillDogSurvey.m +34 -0
  210. package/ios/BillDogSurvey.swift +257 -0
  211. package/ios/BillDogVirtualCurrencyBridge.m +63 -0
  212. package/ios/BillDogVirtualCurrencyBridge.swift +511 -0
  213. package/junit.xml +25 -0
  214. package/package.json +61 -0
  215. package/react-native.config.js +13 -0
  216. package/src/AdvancedPaywallCache.ts +433 -0
  217. package/src/BillDog.ts +983 -0
  218. package/src/BillDogConfiguration.ts +318 -0
  219. package/src/ComponentRenderer.tsx +1325 -0
  220. package/src/LayoutEngine.ts +459 -0
  221. package/src/LocalizationResolver.ts +311 -0
  222. package/src/NativePaywallView.tsx +132 -0
  223. package/src/PaywallCache.ts +358 -0
  224. package/src/PaywallStateManager.ts +154 -0
  225. package/src/PaywallView.tsx +328 -0
  226. package/src/ProductVariableProvider.ts +282 -0
  227. package/src/VariableResolver.ts +516 -0
  228. package/src/WebViewPaywall.tsx +454 -0
  229. package/src/animations/presets.ts +421 -0
  230. package/src/components/AccoladeCpsComponent.tsx +259 -0
  231. package/src/components/AccordionComponent.tsx +213 -0
  232. package/src/components/AnimatedWrapper.tsx +316 -0
  233. package/src/components/CarouselComponent.tsx +615 -0
  234. package/src/components/ChartCurveComponent.tsx +452 -0
  235. package/src/components/CheckboxComponent.tsx +185 -0
  236. package/src/components/ConfettiComponent.tsx +178 -0
  237. package/src/components/CountdownComponent.tsx +454 -0
  238. package/src/components/DelayedXComponent.tsx +266 -0
  239. package/src/components/DrawerModalComponent.tsx +324 -0
  240. package/src/components/GradientView.tsx +129 -0
  241. package/src/components/HapticFeedbackComponent.tsx +39 -0
  242. package/src/components/HorizontalProductsComponent.tsx +329 -0
  243. package/src/components/MultiChoiceGridComponent.tsx +265 -0
  244. package/src/components/NPSScaleComponent.tsx +294 -0
  245. package/src/components/NavigationComponent.tsx +329 -0
  246. package/src/components/OAuthButtonComponent.tsx +266 -0
  247. package/src/components/ProgressLoaderComponent.tsx +366 -0
  248. package/src/components/ProgressStepperComponent.tsx +250 -0
  249. package/src/components/SegmentedPickerComponent.tsx +245 -0
  250. package/src/components/ShareButtonsComponent.tsx +299 -0
  251. package/src/components/ShareCodeComponent.tsx +303 -0
  252. package/src/components/SingleChoiceGridComponent.tsx +229 -0
  253. package/src/components/SliderComponent.tsx +280 -0
  254. package/src/components/StarRatingInputComponent.tsx +223 -0
  255. package/src/components/StickerCompositeComponent.tsx +168 -0
  256. package/src/components/TabsComponent.tsx +496 -0
  257. package/src/components/TextInputComponent.tsx +258 -0
  258. package/src/components/TimelineComponent.tsx +129 -0
  259. package/src/components/ToggleComponent.tsx +142 -0
  260. package/src/components/VerticalProductsComponent.tsx +357 -0
  261. package/src/dynamicValues.ts +180 -0
  262. package/src/filters/dateFilters.ts +411 -0
  263. package/src/filters/index.ts +281 -0
  264. package/src/hooks/useOfferings.ts +192 -0
  265. package/src/hooks/usePaywall.ts +154 -0
  266. package/src/hooks/usePlacement.ts +143 -0
  267. package/src/hooks/useSubscription.ts +154 -0
  268. package/src/index.ts +245 -0
  269. package/src/models/BillDogError.ts +24 -0
  270. package/src/models/CacheFetchPolicy.ts +13 -0
  271. package/src/models/CustomProduct.ts +33 -0
  272. package/src/models/CustomerInfo.ts +26 -0
  273. package/src/models/Delegate.ts +22 -0
  274. package/src/models/EntitlementInfo.ts +27 -0
  275. package/src/models/Placement.ts +50 -0
  276. package/src/models/PurchaseParams.ts +81 -0
  277. package/src/models/PurchaseResult.ts +32 -0
  278. package/src/models/RestoreResult.ts +27 -0
  279. package/src/models/RestoredItem.ts +25 -0
  280. package/src/modules/ABTest.ts +131 -0
  281. package/src/modules/Analytics.ts +227 -0
  282. package/src/modules/Survey.ts +126 -0
  283. package/src/modules/VirtualCurrency.ts +223 -0
  284. package/src/murmur.ts +102 -0
  285. package/src/notifications/LiveActivitiesManager.ts +140 -0
  286. package/src/notifications/NotificationScheduler.ts +292 -0
  287. package/src/notifications/PurchaseNotificationHandler.ts +67 -0
  288. package/src/notifications/useDeviceToken.ts +116 -0
  289. package/src/notifications/useLiveActivity.ts +125 -0
  290. package/src/review/ReviewAnalytics.ts +89 -0
  291. package/src/review/ReviewManager.ts +355 -0
  292. package/src/review/ReviewTimingManager.ts +265 -0
  293. package/src/survey-core/types.ts +244 -0
  294. package/src/types/ambient.d.ts +9 -0
  295. package/src/types.ts +680 -0
  296. package/src/utils/ApiClient.ts +163 -0
  297. package/src/utils/CSSConverter.ts +552 -0
  298. package/src/utils/animations.ts +232 -0
  299. package/src/utils/calendarIntegration.ts +113 -0
  300. package/src/utils/configurationResolver.ts +106 -0
  301. package/src/utils/deviceDetection.ts +83 -0
  302. package/src/utils/hapticFeedback.ts +55 -0
  303. package/src/utils/iconMapper.ts +102 -0
  304. package/src/utils/notificationPermissions.ts +48 -0
  305. package/src/utils/priceFormatting.ts +119 -0
  306. package/src/utils/shareSheet.ts +55 -0
  307. package/src/utils/subscriptionUpgrade.ts +183 -0
  308. package/src/valueUtils.ts +108 -0
  309. 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
+ });