@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,357 @@
1
+ /**
2
+ * Vertical Products Component for React Native
3
+ * Pure Child Model: renders children via renderChild with overlay extraction,
4
+ * child sanitization, and selection state propagation.
5
+ * Matches Android ProductsComponent.kt and iOS ProductsRenderer.swift architecture.
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback } from 'react';
9
+ import {
10
+ View,
11
+ TouchableOpacity,
12
+ ScrollView,
13
+ StyleSheet,
14
+ ViewStyle,
15
+ Text,
16
+ AccessibilityInfo,
17
+ } from 'react-native';
18
+ import { PaywallComponent, PaywallConfiguration } from '../types';
19
+ import { PaywallStateManager } from '../PaywallStateManager';
20
+
21
+ export interface VerticalProductsComponentProps {
22
+ component: PaywallComponent;
23
+ configuration: PaywallConfiguration;
24
+ renderChild: (child: PaywallComponent) => React.ReactNode;
25
+ stateManager?: any;
26
+ onAction?: (componentId: string, action: string) => void;
27
+ }
28
+
29
+ // ============= OVERLAY EXTRACTION =============
30
+
31
+ interface OverlayExtraction {
32
+ badge: PaywallComponent | null;
33
+ checkmark: PaywallComponent | null;
34
+ radioIndicator: PaywallComponent | null;
35
+ remainingChildren: PaywallComponent[];
36
+ }
37
+
38
+ function getRole(child: PaywallComponent): string | undefined {
39
+ return (child as any).role || child.properties?.role;
40
+ }
41
+
42
+ function extractOverlays(children: PaywallComponent[] | undefined): OverlayExtraction {
43
+ if (!children?.length) {
44
+ return { badge: null, checkmark: null, radioIndicator: null, remainingChildren: [] };
45
+ }
46
+
47
+ const badge = children.find(c => getRole(c) === 'badge') || null;
48
+ const checkmark = children.find(c => getRole(c) === 'checkmark') || null;
49
+ const radioIndicator = children.find(c => getRole(c) === 'radio-indicator') || null;
50
+
51
+ const remaining = children.filter(c => c !== badge && c !== checkmark && c !== radioIndicator);
52
+
53
+ return { badge, checkmark, radioIndicator, remainingChildren: remaining };
54
+ }
55
+
56
+ // ============= CHILD SANITIZATION =============
57
+
58
+ function sanitizeProductChild(child: PaywallComponent): PaywallComponent {
59
+ const sanitized = { ...child };
60
+
61
+ // Strip card-level appearance (wrapper handles it)
62
+ if (sanitized.appearance) {
63
+ const { fill, backgroundColor, cornerRadius, borderColor, borderWidth, shadow, ...rest } = sanitized.appearance as any;
64
+ sanitized.appearance = rest;
65
+ }
66
+
67
+ // Strip selected appearance (wrapper handles selection styling)
68
+ (sanitized as any).selectedAppearance = undefined;
69
+
70
+ // Strip layout padding (wrapper applies card padding)
71
+ if (sanitized.layout) {
72
+ const { padding, ...restLayout } = sanitized.layout as any;
73
+ sanitized.layout = restLayout;
74
+ }
75
+
76
+ return sanitized;
77
+ }
78
+
79
+ // ============= SELECTION STATE PROPAGATION =============
80
+
81
+ function applySelectionState(component: PaywallComponent, isSelected: boolean, isHybridMode: boolean): PaywallComponent {
82
+ if (!isHybridMode) return component;
83
+
84
+ const role = getRole(component);
85
+
86
+ // Handle radio-indicator composite
87
+ if (role === 'radio-indicator' && component.type === 'stack' && component.children) {
88
+ return {
89
+ ...component,
90
+ children: component.children.map(iconChild => {
91
+ const iconRole = getRole(iconChild);
92
+ if (iconRole === 'selected-icon') return { ...iconChild, visible: isSelected };
93
+ if (iconRole === 'unselected-icon') return { ...iconChild, visible: !isSelected };
94
+ return applySelectionState(iconChild, isSelected, isHybridMode);
95
+ }),
96
+ };
97
+ }
98
+
99
+ // Handle checkmark visibility
100
+ if (role === 'checkmark') {
101
+ return { ...component, visible: isSelected };
102
+ }
103
+
104
+ // Recurse into children
105
+ if (component.children?.length) {
106
+ return {
107
+ ...component,
108
+ children: component.children.map(c => applySelectionState(c, isSelected, isHybridMode)),
109
+ };
110
+ }
111
+
112
+ return component;
113
+ }
114
+
115
+ // ============= HYBRID MODE DETECTION =============
116
+
117
+ function hasHybridChildren(children: PaywallComponent[]): boolean {
118
+ return children.some(child => {
119
+ if (getRole(child)) return true;
120
+ return hasChildWithRole(child);
121
+ });
122
+ }
123
+
124
+ function hasChildWithRole(component: PaywallComponent): boolean {
125
+ if (!component.children) return false;
126
+ for (const child of component.children) {
127
+ if (getRole(child)) return true;
128
+ if (hasChildWithRole(child)) return true;
129
+ }
130
+ return false;
131
+ }
132
+
133
+ // ============= HELPERS =============
134
+
135
+ function resolveNumber(val: any, fallback: number): number {
136
+ if (typeof val === 'number') return val;
137
+ if (val && typeof val === 'object' && 'value' in val) return val.value;
138
+ return fallback;
139
+ }
140
+
141
+ function resolveColor(appearance: any, key: string, fallback: string): string {
142
+ if (!appearance) return fallback;
143
+ const val = appearance[key];
144
+ if (!val) return fallback;
145
+ if (typeof val === 'string') return val;
146
+ if (val.resolvedColor) return val.resolvedColor;
147
+ if (val.color) return typeof val.color === 'string' ? val.color : fallback;
148
+ if (val.hex) return val.hex;
149
+ return fallback;
150
+ }
151
+
152
+ function resolveFillColor(appearance: any, fallback: string): string {
153
+ if (!appearance) return fallback;
154
+ if (appearance.fill) return resolveColor(appearance, 'fill', fallback);
155
+ if (appearance.backgroundColor) return resolveColor(appearance, 'backgroundColor', fallback);
156
+ return fallback;
157
+ }
158
+
159
+ // ============= MAIN COMPONENT =============
160
+
161
+ export const VerticalProductsComponent: React.FC<VerticalProductsComponentProps> = ({
162
+ component,
163
+ configuration,
164
+ renderChild,
165
+ stateManager,
166
+ onAction,
167
+ }) => {
168
+ const properties = component.properties || {};
169
+ const allChildren = component.children || [];
170
+
171
+ // Package binding
172
+ const bindToPackages = properties.bindToPackages !== false;
173
+ const packageIdentifiers: string[] = properties.packageIdentifiers || [];
174
+
175
+ // Resolve bound packages
176
+ const boundPackages = bindToPackages
177
+ ? (packageIdentifiers.length > 0
178
+ ? packageIdentifiers
179
+ .map(id => (configuration.packages || []).find(p => (p as any).identifier === id || (p as any).productId === id))
180
+ .filter(Boolean)
181
+ : configuration.packages || [])
182
+ : [];
183
+
184
+ // Children resolution: bind mode with packages available -> limit; else show all visible
185
+ const children = (bindToPackages && boundPackages.length > 0)
186
+ ? allChildren.slice(0, boundPackages.length)
187
+ : allChildren.filter(c => c.visible !== false);
188
+
189
+ const gap = resolveNumber(properties.gap, 12);
190
+ const defaultSelectedIndex = resolveNumber(properties.defaultSelectedIndex, 0);
191
+ const showRadio = properties.showRadio !== false;
192
+ const showBadge = !!properties.showBadge;
193
+ const badgeProductIndex = resolveNumber(properties.badgeProductIndex, 0);
194
+
195
+ const isHybridMode = hasHybridChildren(children);
196
+
197
+ // State
198
+ const stateKey = `element:${component.id}.selectedIndex`;
199
+ const [selectedIndex, setSelectedIndex] = useState(defaultSelectedIndex);
200
+
201
+ useEffect(() => {
202
+ if (!stateManager) return;
203
+ const existing = stateManager.getState?.(stateKey);
204
+ if (existing !== undefined && existing !== null) {
205
+ setSelectedIndex(Number(existing));
206
+ } else {
207
+ stateManager.setState?.(stateKey, defaultSelectedIndex);
208
+ }
209
+ // Return the unsubscribe. This was dropped on the floor while nothing supplied a
210
+ // stateManager and every call no-opped; now that one is provided, an uncleaned
211
+ // subscription leaks a listener holding a stale setSelectedIndex on each remount.
212
+ return stateManager.subscribe?.(stateKey, (value: any) => {
213
+ setSelectedIndex(Number(value) || 0);
214
+ });
215
+ }, [component.id, stateManager]);
216
+
217
+ // Publish the live selection PAYWALL-WIDE so Dynamic Values can react to it
218
+ // (`products.selectedIndex == 1` templates) and so `selectedProduct.*` resolves for
219
+ // the user's actual choice. The per-element key above is scoped to this component.
220
+ // With multiple products components, last write wins — same contract as Android
221
+ // ProductsComponent and iOS ProductsRenderer.updateSelectionState.
222
+ useEffect(() => {
223
+ stateManager?.setState?.(PaywallStateManager.SELECTED_INDEX_KEY, selectedIndex);
224
+ }, [selectedIndex, stateManager]);
225
+
226
+ const handleSelect = useCallback((index: number) => {
227
+ setSelectedIndex(index);
228
+ stateManager?.setState?.(stateKey, index);
229
+ onAction?.(component.id, `select_package:${index},${children[index]?.id}`);
230
+
231
+ AccessibilityInfo.announceForAccessibility(`Selected product ${index + 1}`);
232
+ }, [stateKey, stateManager, onAction, component.id, children]);
233
+
234
+ // Render single product card (3-layer structure)
235
+ const renderProductCard = (child: PaywallComponent, index: number) => {
236
+ const isSelected = index === selectedIndex;
237
+ const childAppearance = child.appearance as any;
238
+ const childSelectedAppearance = (child as any).selectedAppearance as any;
239
+ const childLayout = child.layout as any;
240
+
241
+ // Default state styling from child.appearance
242
+ const cardBg = resolveFillColor(childAppearance, 'transparent');
243
+ const cardCornerRadius = resolveNumber(childAppearance?.cornerRadius, 12);
244
+ const cardBorderColor = resolveColor(childAppearance, 'borderColor', 'rgba(128,128,128,0.2)');
245
+ const cardBorderWidth = resolveNumber(childAppearance?.borderWidth, 0);
246
+
247
+ // Selected state styling from child.selectedAppearance
248
+ const selectedBg = resolveFillColor(childSelectedAppearance, 'rgba(59,130,246,0.1)');
249
+ const selectedBorderColor = resolveColor(childSelectedAppearance, 'borderColor', '#3B82F6');
250
+ const selectedBorderWidth = resolveNumber(childSelectedAppearance?.borderWidth, 2);
251
+
252
+ const bgColor = isSelected ? selectedBg : cardBg;
253
+ const borderColor = isSelected ? selectedBorderColor : cardBorderColor;
254
+ const borderWidth = isSelected ? selectedBorderWidth : cardBorderWidth;
255
+
256
+ // Padding from child.layout.padding
257
+ const padding = childLayout?.padding;
258
+ const paddingTop = resolveNumber(padding?.top, 14);
259
+ const paddingRight = resolveNumber(padding?.right, 16);
260
+ const paddingBottom = resolveNumber(padding?.bottom, 14);
261
+ const paddingLeft = resolveNumber(padding?.left, 16);
262
+
263
+ // Extract overlays
264
+ const extraction = extractOverlays(child.children);
265
+
266
+ // Sanitize child
267
+ const childWithRemaining = { ...child, children: extraction.remainingChildren };
268
+ const sanitized = sanitizeProductChild(childWithRemaining);
269
+ const transformed = applySelectionState(sanitized, isSelected, isHybridMode);
270
+
271
+ return (
272
+ <TouchableOpacity
273
+ key={child.id || `product-${index}`}
274
+ activeOpacity={0.8}
275
+ onPress={() => handleSelect(index)}
276
+ accessibilityRole="radio"
277
+ accessibilityState={{ selected: isSelected }}
278
+ accessibilityLabel={`Product ${index + 1}, ${isSelected ? 'selected' : 'not selected'}`}
279
+ style={{ marginBottom: index < children.length - 1 ? gap : 0 }}
280
+ >
281
+ {/* 3-Layer Structure */}
282
+ <View style={{ position: 'relative' }}>
283
+ {/* Layer 1: Background */}
284
+ <View
285
+ style={{
286
+ ...StyleSheet.absoluteFillObject,
287
+ backgroundColor: bgColor,
288
+ borderRadius: cardCornerRadius,
289
+ borderColor: borderColor,
290
+ borderWidth: borderWidth,
291
+ }}
292
+ />
293
+
294
+ {/* Layer 2: Content with padding */}
295
+ <View
296
+ style={{
297
+ flexDirection: 'row',
298
+ alignItems: 'center',
299
+ paddingTop,
300
+ paddingRight,
301
+ paddingBottom,
302
+ paddingLeft,
303
+ }}
304
+ >
305
+ {/* Radio indicator */}
306
+ {showRadio && (
307
+ isHybridMode && extraction.radioIndicator ? (
308
+ <View style={{ marginRight: 12 }}>
309
+ {renderChild(applySelectionState(extraction.radioIndicator, isSelected, true))}
310
+ </View>
311
+ ) : !isHybridMode ? (
312
+ <View style={{ marginRight: 12 }}>
313
+ <View
314
+ style={{
315
+ width: 22,
316
+ height: 22,
317
+ borderRadius: 11,
318
+ borderWidth: 2,
319
+ borderColor: isSelected ? '#3B82F6' : 'rgba(128,128,128,0.4)',
320
+ backgroundColor: isSelected ? '#3B82F6' : 'transparent',
321
+ justifyContent: 'center',
322
+ alignItems: 'center',
323
+ }}
324
+ >
325
+ {isSelected && (
326
+ <Text style={{ color: '#FFFFFF', fontSize: 12, fontWeight: 'bold' }}>✓</Text>
327
+ )}
328
+ </View>
329
+ </View>
330
+ ) : null
331
+ )}
332
+
333
+ {/* Render sanitized child content */}
334
+ <View style={{ flex: 1 }}>
335
+ {renderChild(transformed)}
336
+ </View>
337
+ </View>
338
+
339
+ {/* Layer 3: Badge overlay */}
340
+ {extraction.badge && showBadge && index === badgeProductIndex && (
341
+ <View style={{ position: 'absolute', top: -10, right: 12, zIndex: 10 }}>
342
+ {renderChild(extraction.badge)}
343
+ </View>
344
+ )}
345
+ </View>
346
+ </TouchableOpacity>
347
+ );
348
+ };
349
+
350
+ return (
351
+ <View accessibilityRole="radiogroup" accessibilityLabel="Select a product">
352
+ {children.map((child, index) => renderProductCard(child, index))}
353
+ </View>
354
+ );
355
+ };
356
+
357
+ export default VerticalProductsComponent;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Dynamic Values System for React Native SDK
3
+ * Supports nested condition groups with AND/OR logic
4
+ */
5
+
6
+ import { getNestedValue, isEqual, toNumber, stringify } from './valueUtils';
7
+ export interface DynamicCondition {
8
+ id: string;
9
+ type: 'variable';
10
+ variableName?: string;
11
+ operator: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'greater_than_or_equal' | 'less_than_or_equal' | 'contains' | 'exists';
12
+ value: any;
13
+ }
14
+
15
+ export interface ConditionGroup {
16
+ id: string;
17
+ logic: 'AND' | 'OR';
18
+ conditions: DynamicCondition[];
19
+ groups: ConditionGroup[]; // Recursive nested groups
20
+ }
21
+
22
+ export interface DynamicValueEntry {
23
+ id: string;
24
+ rootGroup: ConditionGroup; // Required - nested condition groups
25
+ value: any;
26
+ }
27
+
28
+
29
+ export interface DynamicValue {
30
+ _isDynamic: true;
31
+ default: any;
32
+ values: DynamicValueEntry[];
33
+ }
34
+
35
+ export function isDynamicValue(value: any): value is DynamicValue {
36
+ return typeof value === 'object' && value !== null && value._isDynamic === true;
37
+ }
38
+
39
+ export function evaluateDynamicValue(dynamicValue: any, variables: Record<string, any>): any {
40
+ if (!isDynamicValue(dynamicValue)) return dynamicValue;
41
+
42
+ try {
43
+ const entries = dynamicValue.values || [];
44
+ for (const entry of entries) {
45
+ if (evaluateConditionGroup(entry.rootGroup, variables)) {
46
+ return entry.value;
47
+ }
48
+ }
49
+ return dynamicValue.default;
50
+ } catch (error) {
51
+ console.warn('[DynamicValue] Evaluation failed, using default:', error);
52
+ return dynamicValue.default;
53
+ }
54
+ }
55
+
56
+ export function evaluateConditionGroup(group: ConditionGroup, variables: Record<string, any>): boolean {
57
+ // `?? []` — a group commonly omits `conditions` or `groups` when empty (the conformance corpus and
58
+ // DB-stored dynamic values both do). `.map` on the missing array throws, the throw is swallowed by
59
+ // evaluateDynamicValue's try/catch, and the value silently collapses to its default. The web copy had
60
+ // this guard; it was never propagated here.
61
+ const conditionResults = (group.conditions ?? []).map(c => evaluateSingleCondition(c, variables));
62
+ const groupResults = (group.groups ?? []).map(g => evaluateConditionGroup(g, variables));
63
+ const allResults = [...conditionResults, ...groupResults];
64
+
65
+ // Empty group evaluates to true
66
+ if (allResults.length === 0) return true;
67
+
68
+ return group.logic === 'AND'
69
+ ? allResults.every(Boolean)
70
+ : allResults.some(Boolean);
71
+ }
72
+
73
+ function evaluateSingleCondition(condition: DynamicCondition, variables: Record<string, any>): boolean {
74
+ if (!condition.variableName) return false;
75
+ const varValue = getNestedValue(variables, condition.variableName);
76
+
77
+ switch (condition.operator) {
78
+ case 'equals': return isEqual(varValue, condition.value);
79
+ case 'not_equals': return !isEqual(varValue, condition.value);
80
+ // toNumber (not Number()): non-numeric → 0, matching native `toDouble`. `Number()` yields NaN, which
81
+ // makes every ordering comparison silently false and disagrees with the device.
82
+ case 'greater_than': return toNumber(varValue) > toNumber(condition.value);
83
+ case 'less_than': return toNumber(varValue) < toNumber(condition.value);
84
+ case 'greater_than_or_equal': return toNumber(varValue) >= toNumber(condition.value);
85
+ case 'less_than_or_equal': return toNumber(varValue) <= toNumber(condition.value);
86
+ case 'contains':
87
+ if (varValue == null) return false;
88
+ return stringify(varValue).toLowerCase().includes(stringify(condition.value).toLowerCase());
89
+ case 'exists': return varValue != null && stringify(varValue) !== '';
90
+ default: return false;
91
+ }
92
+ }
93
+
94
+ export function processComponentProperties(properties: Record<string, any>, variables: Record<string, any>): Record<string, any> {
95
+ const safeProps = (properties && typeof properties === 'object') ? properties : {};
96
+ const processed: Record<string, any> = {};
97
+ for (const [key, value] of Object.entries(safeProps)) {
98
+ if (isDynamicValue(value)) {
99
+ processed[key] = evaluateDynamicValue(value, variables);
100
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
101
+ processed[key] = processComponentProperties(value as Record<string, any>, variables);
102
+ } else {
103
+ processed[key] = value;
104
+ }
105
+ }
106
+ return processed;
107
+ }
108
+
109
+ export function processFullComponent<T extends Record<string, any>>(
110
+ component: T,
111
+ variables: Record<string, any>
112
+ ): T {
113
+ const result = { ...component };
114
+
115
+ const nestedKeys = ['properties', 'appearance', 'size', 'containerLayout', 'layout', 'border', 'shadow', 'selectedAppearance', 'selectedProperties', 'selectedLayout', 'selectedSize', 'activeAppearance', 'completedAppearance', 'tapBehavior'];
116
+ for (const key of nestedKeys) {
117
+ if (component[key] && typeof component[key] === 'object') {
118
+ (result as any)[key] = processComponentProperties(component[key], variables);
119
+ }
120
+ }
121
+
122
+ if (isDynamicValue(component.visible)) {
123
+ (result as any).visible = evaluateDynamicValue(component.visible, variables);
124
+ }
125
+
126
+ // Recursive children processing
127
+ if (Array.isArray(component.children)) {
128
+ (result as any).children = component.children.map(
129
+ (child: any) => processFullComponent(child, variables)
130
+ );
131
+ }
132
+
133
+ return result;
134
+ }
135
+
136
+ // Factory functions
137
+ export function createConditionGroup(logic: 'AND' | 'OR' = 'AND'): ConditionGroup {
138
+ return {
139
+ id: generateId(),
140
+ logic,
141
+ conditions: [],
142
+ groups: [],
143
+ };
144
+ }
145
+
146
+ export function createDynamicValue(defaultValue: any, values: DynamicValueEntry[] = []): DynamicValue {
147
+ return { _isDynamic: true, default: defaultValue, values };
148
+ }
149
+
150
+ export function createCondition(
151
+ variableName: string,
152
+ operator: DynamicCondition['operator'],
153
+ value: any
154
+ ): DynamicCondition {
155
+ return {
156
+ id: generateId(),
157
+ type: 'variable',
158
+ variableName,
159
+ operator,
160
+ value,
161
+ };
162
+ }
163
+
164
+ export function createSimpleValueEntry(variableName: string, operator: DynamicCondition['operator'], value: any, returnValue: any): DynamicValueEntry {
165
+ const condition = createCondition(variableName, operator, value);
166
+ const group: ConditionGroup = {
167
+ ...createConditionGroup('AND'),
168
+ conditions: [condition],
169
+ };
170
+ return {
171
+ id: generateId(),
172
+ rootGroup: group,
173
+ value: returnValue,
174
+ };
175
+ }
176
+
177
+
178
+ function generateId(): string {
179
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
180
+ }