@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,39 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { View } from 'react-native';
3
+ import { PaywallComponent } from '../types';
4
+ import { triggerHaptic, HapticType } from '../utils/hapticFeedback';
5
+
6
+ interface HapticFeedbackComponentProps {
7
+ component: PaywallComponent;
8
+ isPreviewMode?: boolean;
9
+ }
10
+
11
+ const HAPTIC_STYLES: ReadonlySet<string> = new Set([
12
+ 'light', 'medium', 'heavy', 'success', 'warning', 'error',
13
+ ]);
14
+
15
+ /**
16
+ * HapticFeedbackComponent — invisible behavior component.
17
+ *
18
+ * Fires a single device haptic on mount, reusing the shared `triggerHaptic`
19
+ * util (the same one the haptic tap-action uses). Skipped in the builder
20
+ * preview. The style comes from `properties.hapticStyle` (the six canonical
21
+ * HapticType values), falling back to `medium`. Renders nothing visible.
22
+ */
23
+ export const HapticFeedbackComponent: React.FC<HapticFeedbackComponentProps> = ({
24
+ component,
25
+ isPreviewMode = false,
26
+ }) => {
27
+ const hasFiredRef = useRef(false);
28
+
29
+ useEffect(() => {
30
+ if (isPreviewMode || hasFiredRef.current) return;
31
+ hasFiredRef.current = true;
32
+ const props = (component.properties as Record<string, unknown>) || {};
33
+ const raw = typeof props.hapticStyle === 'string' ? props.hapticStyle : 'medium';
34
+ const style = (HAPTIC_STYLES.has(raw) ? raw : 'medium') as HapticType;
35
+ triggerHaptic(style);
36
+ }, [component.id, isPreviewMode]);
37
+
38
+ return <View style={{ width: 0, height: 0 }} />;
39
+ };
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Horizontal Products Component for React Native
3
+ * Pure Child Model: renders children via renderChild with horizontal FlatList layout,
4
+ * overlay extraction, child sanitization, and selection state propagation.
5
+ * Matches Android ProductsComponent.kt and iOS ProductsRenderer.swift architecture.
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
9
+ import {
10
+ View,
11
+ TouchableOpacity,
12
+ FlatList,
13
+ StyleSheet,
14
+ Text,
15
+ AccessibilityInfo,
16
+ } from 'react-native';
17
+ import { PaywallComponent, PaywallConfiguration } from '../types';
18
+ import { PaywallStateManager } from '../PaywallStateManager';
19
+
20
+ export interface HorizontalProductsComponentProps {
21
+ component: PaywallComponent;
22
+ configuration: PaywallConfiguration;
23
+ renderChild: (child: PaywallComponent) => React.ReactNode;
24
+ stateManager?: any;
25
+ onAction?: (componentId: string, action: string) => void;
26
+ }
27
+
28
+ // ============= OVERLAY EXTRACTION =============
29
+
30
+ interface OverlayExtraction {
31
+ badge: PaywallComponent | null;
32
+ checkmark: PaywallComponent | null;
33
+ radioIndicator: PaywallComponent | null;
34
+ remainingChildren: PaywallComponent[];
35
+ }
36
+
37
+ function getRole(child: PaywallComponent): string | undefined {
38
+ return (child as any).role || child.properties?.role;
39
+ }
40
+
41
+ function extractOverlays(children: PaywallComponent[] | undefined): OverlayExtraction {
42
+ if (!children?.length) {
43
+ return { badge: null, checkmark: null, radioIndicator: null, remainingChildren: [] };
44
+ }
45
+
46
+ const badge = children.find(c => getRole(c) === 'badge') || null;
47
+ const checkmark = children.find(c => getRole(c) === 'checkmark') || null;
48
+ const radioIndicator = children.find(c => getRole(c) === 'radio-indicator') || null;
49
+
50
+ const remaining = children.filter(c => c !== badge && c !== checkmark && c !== radioIndicator);
51
+
52
+ return { badge, checkmark, radioIndicator, remainingChildren: remaining };
53
+ }
54
+
55
+ // ============= CHILD SANITIZATION =============
56
+
57
+ function sanitizeProductChild(child: PaywallComponent): PaywallComponent {
58
+ const sanitized = { ...child };
59
+
60
+ if (sanitized.appearance) {
61
+ const { fill, backgroundColor, cornerRadius, borderColor, borderWidth, shadow, ...rest } = sanitized.appearance as any;
62
+ sanitized.appearance = rest;
63
+ }
64
+
65
+ (sanitized as any).selectedAppearance = undefined;
66
+
67
+ if (sanitized.layout) {
68
+ const { padding, ...restLayout } = sanitized.layout as any;
69
+ sanitized.layout = restLayout;
70
+ }
71
+
72
+ return sanitized;
73
+ }
74
+
75
+ // ============= SELECTION STATE PROPAGATION =============
76
+
77
+ function applySelectionState(component: PaywallComponent, isSelected: boolean, isHybridMode: boolean): PaywallComponent {
78
+ if (!isHybridMode) return component;
79
+
80
+ const role = getRole(component);
81
+
82
+ if (role === 'radio-indicator' && component.type === 'stack' && component.children) {
83
+ return {
84
+ ...component,
85
+ children: component.children.map(iconChild => {
86
+ const iconRole = getRole(iconChild);
87
+ if (iconRole === 'selected-icon') return { ...iconChild, visible: isSelected };
88
+ if (iconRole === 'unselected-icon') return { ...iconChild, visible: !isSelected };
89
+ return applySelectionState(iconChild, isSelected, isHybridMode);
90
+ }),
91
+ };
92
+ }
93
+
94
+ if (role === 'checkmark') {
95
+ return { ...component, visible: isSelected };
96
+ }
97
+
98
+ if (component.children?.length) {
99
+ return {
100
+ ...component,
101
+ children: component.children.map(c => applySelectionState(c, isSelected, isHybridMode)),
102
+ };
103
+ }
104
+
105
+ return component;
106
+ }
107
+
108
+ // ============= HYBRID MODE DETECTION =============
109
+
110
+ function hasHybridChildren(children: PaywallComponent[]): boolean {
111
+ return children.some(child => {
112
+ if (getRole(child)) return true;
113
+ return hasChildWithRole(child);
114
+ });
115
+ }
116
+
117
+ function hasChildWithRole(component: PaywallComponent): boolean {
118
+ if (!component.children) return false;
119
+ for (const child of component.children) {
120
+ if (getRole(child)) return true;
121
+ if (hasChildWithRole(child)) return true;
122
+ }
123
+ return false;
124
+ }
125
+
126
+ // ============= HELPERS =============
127
+
128
+ function resolveNumber(val: any, fallback: number): number {
129
+ if (typeof val === 'number') return val;
130
+ if (val && typeof val === 'object' && 'value' in val) return val.value;
131
+ return fallback;
132
+ }
133
+
134
+ function resolveColor(appearance: any, key: string, fallback: string): string {
135
+ if (!appearance) return fallback;
136
+ const val = appearance[key];
137
+ if (!val) return fallback;
138
+ if (typeof val === 'string') return val;
139
+ if (val.resolvedColor) return val.resolvedColor;
140
+ if (val.color) return typeof val.color === 'string' ? val.color : fallback;
141
+ if (val.hex) return val.hex;
142
+ return fallback;
143
+ }
144
+
145
+ function resolveFillColor(appearance: any, fallback: string): string {
146
+ if (!appearance) return fallback;
147
+ if (appearance.fill) return resolveColor(appearance, 'fill', fallback);
148
+ if (appearance.backgroundColor) return resolveColor(appearance, 'backgroundColor', fallback);
149
+ return fallback;
150
+ }
151
+
152
+ // ============= MAIN COMPONENT =============
153
+
154
+ export const HorizontalProductsComponent: React.FC<HorizontalProductsComponentProps> = ({
155
+ component,
156
+ configuration,
157
+ renderChild,
158
+ stateManager,
159
+ onAction,
160
+ }) => {
161
+ const properties = component.properties || {};
162
+ const allChildren = component.children || [];
163
+ const flatListRef = useRef<FlatList>(null);
164
+
165
+ // Package binding
166
+ const bindToPackages = properties.bindToPackages !== false;
167
+ const packageIdentifiers: string[] = properties.packageIdentifiers || [];
168
+
169
+ const boundPackages = bindToPackages
170
+ ? (packageIdentifiers.length > 0
171
+ ? packageIdentifiers
172
+ .map(id => (configuration.packages || []).find(p => (p as any).identifier === id || (p as any).productId === id))
173
+ .filter(Boolean)
174
+ : configuration.packages || [])
175
+ : [];
176
+
177
+ const children = (bindToPackages && boundPackages.length > 0)
178
+ ? allChildren.slice(0, boundPackages.length)
179
+ : allChildren.filter(c => c.visible !== false);
180
+
181
+ const gap = resolveNumber(properties.gap, 12);
182
+ const defaultSelectedIndex = resolveNumber(properties.defaultSelectedIndex, 0);
183
+ const cardWidth = resolveNumber(properties.cardWidth, 160);
184
+ const cardHeight = resolveNumber(properties.cardHeight, 200);
185
+ const showBadge = !!properties.showBadge;
186
+ const badgeProductIndex = resolveNumber(properties.badgeProductIndex, 0);
187
+ const enableSnapScrolling = properties.enableSnapScrolling !== false;
188
+
189
+ const isHybridMode = hasHybridChildren(children);
190
+
191
+ // State
192
+ const stateKey = `element:${component.id}.selectedIndex`;
193
+ const [selectedIndex, setSelectedIndex] = useState(defaultSelectedIndex);
194
+
195
+ useEffect(() => {
196
+ if (!stateManager) return;
197
+ const existing = stateManager.getState?.(stateKey);
198
+ if (existing !== undefined && existing !== null) {
199
+ setSelectedIndex(Number(existing));
200
+ } else {
201
+ stateManager.setState?.(stateKey, defaultSelectedIndex);
202
+ }
203
+ // Return the unsubscribe. This was dropped on the floor while nothing supplied a
204
+ // stateManager and every call no-opped; now that one is provided, an uncleaned
205
+ // subscription leaks a listener holding a stale setSelectedIndex on each remount.
206
+ return stateManager.subscribe?.(stateKey, (value: any) => {
207
+ setSelectedIndex(Number(value) || 0);
208
+ });
209
+ }, [component.id, stateManager]);
210
+
211
+ // Publish the live selection PAYWALL-WIDE so Dynamic Values can react to it
212
+ // (`products.selectedIndex == 1` templates) and so `selectedProduct.*` resolves for
213
+ // the user's actual choice. The per-element key above is scoped to this component.
214
+ // With multiple products components, last write wins — same contract as Android
215
+ // ProductsComponent and iOS ProductsRenderer.updateSelectionState.
216
+ useEffect(() => {
217
+ stateManager?.setState?.(PaywallStateManager.SELECTED_INDEX_KEY, selectedIndex);
218
+ }, [selectedIndex, stateManager]);
219
+
220
+ const handleSelect = useCallback((index: number) => {
221
+ setSelectedIndex(index);
222
+ stateManager?.setState?.(stateKey, index);
223
+ onAction?.(component.id, `select_package:${index},${children[index]?.id}`);
224
+
225
+ AccessibilityInfo.announceForAccessibility(`Selected product ${index + 1}`);
226
+ }, [stateKey, stateManager, onAction, component.id, children]);
227
+
228
+ const renderProductCard = (child: PaywallComponent, index: number) => {
229
+ const isSelected = index === selectedIndex;
230
+ const childAppearance = child.appearance as any;
231
+ const childSelectedAppearance = (child as any).selectedAppearance as any;
232
+ const childLayout = child.layout as any;
233
+
234
+ const cardBg = resolveFillColor(childAppearance, 'transparent');
235
+ const cardCornerRadius = resolveNumber(childAppearance?.cornerRadius, 12);
236
+ const cardBorderColor = resolveColor(childAppearance, 'borderColor', 'rgba(128,128,128,0.2)');
237
+ const cardBorderWidth = resolveNumber(childAppearance?.borderWidth, 0);
238
+
239
+ const selectedBg = resolveFillColor(childSelectedAppearance, 'rgba(59,130,246,0.1)');
240
+ const selectedBorderColor = resolveColor(childSelectedAppearance, 'borderColor', '#3B82F6');
241
+ const selectedBorderWidth = resolveNumber(childSelectedAppearance?.borderWidth, 2);
242
+
243
+ const bgColor = isSelected ? selectedBg : cardBg;
244
+ const borderColorVal = isSelected ? selectedBorderColor : cardBorderColor;
245
+ const borderWidthVal = isSelected ? selectedBorderWidth : cardBorderWidth;
246
+
247
+ // Padding from child.layout.padding
248
+ const padding = childLayout?.padding;
249
+ const paddingTop = resolveNumber(padding?.top, 14);
250
+ const paddingRight = resolveNumber(padding?.right, 16);
251
+ const paddingBottom = resolveNumber(padding?.bottom, 14);
252
+ const paddingLeft = resolveNumber(padding?.left, 16);
253
+
254
+ const extraction = extractOverlays(child.children);
255
+ const childWithRemaining = { ...child, children: extraction.remainingChildren };
256
+ const sanitized = sanitizeProductChild(childWithRemaining);
257
+ const transformed = applySelectionState(sanitized, isSelected, isHybridMode);
258
+
259
+ return (
260
+ <TouchableOpacity
261
+ activeOpacity={0.8}
262
+ onPress={() => handleSelect(index)}
263
+ accessibilityRole="radio"
264
+ accessibilityState={{ selected: isSelected }}
265
+ accessibilityLabel={`Product ${index + 1}, ${isSelected ? 'selected' : 'not selected'}`}
266
+ style={{ marginRight: index < children.length - 1 ? gap : 0 }}
267
+ >
268
+ <View style={{ width: cardWidth, height: cardHeight, position: 'relative' }}>
269
+ {/* Layer 1: Background */}
270
+ <View
271
+ style={{
272
+ ...StyleSheet.absoluteFillObject,
273
+ backgroundColor: bgColor,
274
+ borderRadius: cardCornerRadius,
275
+ borderColor: borderColorVal,
276
+ borderWidth: borderWidthVal,
277
+ }}
278
+ />
279
+
280
+ {/* Layer 2: Content */}
281
+ <View
282
+ style={{
283
+ flex: 1,
284
+ alignItems: 'center',
285
+ justifyContent: 'center',
286
+ paddingTop,
287
+ paddingRight,
288
+ paddingBottom,
289
+ paddingLeft,
290
+ }}
291
+ >
292
+ {renderChild(transformed)}
293
+ </View>
294
+
295
+ {/* Layer 3: Badge overlay */}
296
+ {extraction.badge && showBadge && index === badgeProductIndex && (
297
+ <View style={{ position: 'absolute', top: -10, right: 12, zIndex: 10 }}>
298
+ {renderChild(extraction.badge)}
299
+ </View>
300
+ )}
301
+ </View>
302
+ </TouchableOpacity>
303
+ );
304
+ };
305
+
306
+ return (
307
+ <View accessibilityRole="radiogroup" accessibilityLabel="Select a product">
308
+ <FlatList
309
+ ref={flatListRef}
310
+ data={children}
311
+ renderItem={({ item, index }) => renderProductCard(item, index)}
312
+ keyExtractor={(item, index) => item.id || `product-${index}`}
313
+ horizontal
314
+ showsHorizontalScrollIndicator={false}
315
+ snapToInterval={enableSnapScrolling ? cardWidth + gap : undefined}
316
+ snapToAlignment="start"
317
+ decelerationRate={enableSnapScrolling ? 'fast' : 'normal'}
318
+ contentContainerStyle={{ paddingTop: 12 }}
319
+ getItemLayout={(_, index) => ({
320
+ length: cardWidth + gap,
321
+ offset: (cardWidth + gap) * index,
322
+ index,
323
+ })}
324
+ />
325
+ </View>
326
+ );
327
+ };
328
+
329
+ export default HorizontalProductsComponent;
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Multi Choice Grid Component for React Native
3
+ * Checkbox style grid selection (multiple selection)
4
+ */
5
+
6
+ import React, { useState, useEffect } from 'react';
7
+ import {
8
+ View,
9
+ Text,
10
+ TouchableOpacity,
11
+ StyleSheet,
12
+ ViewStyle,
13
+ TextStyle,
14
+ DimensionValue,
15
+ } from 'react-native';
16
+ import Svg, { Path } from 'react-native-svg';
17
+ import { PaywallComponent } from '../types';
18
+
19
+ interface MultiChoiceGridProps {
20
+ component: PaywallComponent;
21
+ stateManager?: any;
22
+ isPreviewMode?: boolean;
23
+ }
24
+
25
+ const SIZE_MAP = { sm: 16, md: 20, lg: 24 };
26
+
27
+ // Check icon paths
28
+ const CHECK_PATHS = {
29
+ check: 'M20 6L9 17L4 12',
30
+ cross: 'M18 6L6 18M6 6L18 18',
31
+ circle: 'M12 12m-8 0a8 8 0 1 0 16 0a8 8 0 1 0 -16 0',
32
+ };
33
+
34
+ export const MultiChoiceGridComponent: React.FC<MultiChoiceGridProps> = ({
35
+ component,
36
+ stateManager,
37
+ isPreviewMode = false,
38
+ }) => {
39
+ const props = component.properties || {};
40
+
41
+ // Parse options
42
+ const optionsStr = (props as any).options || 'Option 1,Option 2,Option 3,Option 4';
43
+ const options = optionsStr.split(',').map((o: string) => o.trim()).filter(Boolean);
44
+ const descriptionsStr = (props as any).optionDescriptions || '';
45
+ const descriptions = descriptionsStr.split(',').map((d: string) => d.trim());
46
+
47
+ // Grid settings
48
+ const columns = (props as any).columns || 2;
49
+ const gap = parseFloat((props as any).gap?.value || (props as any).gap) || 12;
50
+ const minItemHeight = parseFloat((props as any).minItemHeight?.value || (props as any).minItemHeight) || 80;
51
+
52
+ // Selection settings
53
+ const defaultIndicesStr = (props as any).defaultSelectedIndices || '';
54
+ const defaultIndices = defaultIndicesStr
55
+ .split(',')
56
+ .map((s: string) => parseInt(s.trim(), 10))
57
+ .filter((n: number) => !isNaN(n));
58
+ const minSelections = (props as any).minSelections || 0;
59
+ const maxSelections = (props as any).maxSelections || 0;
60
+ const disabled = (props as any).disabled ?? false;
61
+
62
+ // State
63
+ const [selectedIndices, setSelectedIndices] = useState<number[]>(defaultIndices);
64
+
65
+ // State manager integration
66
+ useEffect(() => {
67
+ if (!stateManager) return;
68
+
69
+ const stateId = `element:${component.id}.selectedIndices`;
70
+ stateManager.registerState?.(stateId, selectedIndices);
71
+
72
+ return stateManager.subscribeToState?.(stateId, (value: number[]) => {
73
+ if (JSON.stringify(value) !== JSON.stringify(selectedIndices)) {
74
+ setSelectedIndices(value);
75
+ }
76
+ });
77
+ }, [component.id, selectedIndices, stateManager]);
78
+
79
+ const handleSelect = (index: number) => {
80
+ if (disabled) return;
81
+
82
+ let newIndices: number[];
83
+
84
+ if (selectedIndices.includes(index)) {
85
+ if (minSelections > 0 && selectedIndices.length <= minSelections) {
86
+ return;
87
+ }
88
+ newIndices = selectedIndices.filter(i => i !== index);
89
+ } else {
90
+ if (maxSelections > 0 && selectedIndices.length >= maxSelections) {
91
+ return;
92
+ }
93
+ newIndices = [...selectedIndices, index].sort((a, b) => a - b);
94
+ }
95
+
96
+ setSelectedIndices(newIndices);
97
+
98
+ stateManager?.updateState?.(`element:${component.id}.selectedIndices`, newIndices);
99
+ stateManager?.updateState?.(`element:${component.id}.selectedValues`, newIndices.map(i => options[i]));
100
+ };
101
+
102
+ // Styling
103
+ const showIndicator = (props as any).showIndicator ?? true;
104
+ const indicatorPosition = (props as any).indicatorPosition || 'top-right';
105
+ const checkIcon = (props as any).checkIcon || 'check';
106
+ const indicatorSize = SIZE_MAP[(props as any).indicatorSize as keyof typeof SIZE_MAP] || SIZE_MAP.md;
107
+ const indicatorColor = (props as any).indicatorColor || '#ffffff';
108
+ const indicatorBackground = (props as any).indicatorBackground || '#007AFF';
109
+ const indicatorBorderColor = (props as any).indicatorBorderColor || '#d1d5db';
110
+
111
+ const cardBorderRadius = parseFloat((props as any).cardBorderRadius?.value || (props as any).cardBorderRadius) || 12;
112
+ const cardBorderColor = (props as any).cardBorderColor || '#e5e7eb';
113
+ const selectedBorderColor = (props as any).selectedBorderColor || '#007AFF';
114
+
115
+ const labelFontSize = parseFloat((props as any).labelFontSize?.value || (props as any).labelFontSize) || 16;
116
+ const labelFontWeight = String((props as any).labelFontWeight || '500') as TextStyle['fontWeight'];
117
+ const labelColor = (props as any).labelColor || '#1f2937';
118
+ const selectedLabelColor = (props as any).selectedLabelColor || '#007AFF';
119
+ const descriptionFontSize = parseFloat((props as any).descriptionFontSize?.value || (props as any).descriptionFontSize) || 12;
120
+ const descriptionColor = (props as any).descriptionColor || '#6b7280';
121
+
122
+ // Background color helper
123
+ const getBackgroundColor = (fill: any): string => {
124
+ if (!fill) return '#ffffff';
125
+ if (typeof fill === 'string') return fill;
126
+ if (fill.type === 'color') return fill.color || '#ffffff';
127
+ if (fill.resolvedColor) return fill.resolvedColor;
128
+ return '#ffffff';
129
+ };
130
+
131
+ const cardBackground = getBackgroundColor((props as any).cardBackground);
132
+ const selectedCardBackground = getBackgroundColor((props as any).selectedCardBackground);
133
+
134
+ // Indicator position style
135
+ const getIndicatorPositionStyle = (): ViewStyle => {
136
+ const base: ViewStyle = { position: 'absolute' };
137
+ switch (indicatorPosition) {
138
+ case 'top-left': return { ...base, top: 8, left: 8 };
139
+ case 'top-right': return { ...base, top: 8, right: 8 };
140
+ case 'bottom-left': return { ...base, bottom: 8, left: 8 };
141
+ case 'bottom-right': return { ...base, bottom: 8, right: 8 };
142
+ default: return { ...base, top: 8, right: 8 };
143
+ }
144
+ };
145
+
146
+ // Calculate item width based on columns
147
+ const itemWidth: DimensionValue = `${(100 - (columns - 1) * 2) / columns}%`;
148
+
149
+ return (
150
+ <View style={[styles.container, { opacity: disabled ? 0.6 : 1 }]}>
151
+ <View style={[styles.grid, { gap }]}>
152
+ {options.map((option: string, index: number) => {
153
+ const isSelected = selectedIndices.includes(index);
154
+ const description = descriptions[index] || '';
155
+
156
+ return (
157
+ <TouchableOpacity
158
+ key={index}
159
+ onPress={() => handleSelect(index)}
160
+ disabled={disabled}
161
+ activeOpacity={0.7}
162
+ style={[
163
+ styles.card,
164
+ {
165
+ width: itemWidth,
166
+ minHeight: minItemHeight,
167
+ backgroundColor: isSelected ? selectedCardBackground : cardBackground,
168
+ borderColor: isSelected ? selectedBorderColor : cardBorderColor,
169
+ borderRadius: cardBorderRadius,
170
+ },
171
+ ]}
172
+ >
173
+ {/* Checkbox Indicator */}
174
+ {showIndicator && (
175
+ <View
176
+ style={[
177
+ getIndicatorPositionStyle(),
178
+ {
179
+ width: indicatorSize,
180
+ height: indicatorSize,
181
+ borderRadius: 4,
182
+ borderWidth: 2,
183
+ borderColor: isSelected ? indicatorBackground : indicatorBorderColor,
184
+ backgroundColor: isSelected ? indicatorBackground : 'transparent',
185
+ alignItems: 'center',
186
+ justifyContent: 'center',
187
+ },
188
+ ]}
189
+ >
190
+ {isSelected && (
191
+ <Svg
192
+ width={indicatorSize * 0.7}
193
+ height={indicatorSize * 0.7}
194
+ viewBox="0 0 24 24"
195
+ fill="none"
196
+ >
197
+ <Path
198
+ d={CHECK_PATHS[checkIcon as keyof typeof CHECK_PATHS] || CHECK_PATHS.check}
199
+ stroke={indicatorColor}
200
+ strokeWidth={3}
201
+ strokeLinecap="round"
202
+ strokeLinejoin="round"
203
+ />
204
+ </Svg>
205
+ )}
206
+ </View>
207
+ )}
208
+
209
+ {/* Label */}
210
+ <Text
211
+ style={[
212
+ styles.label,
213
+ {
214
+ fontSize: labelFontSize,
215
+ fontWeight: labelFontWeight,
216
+ color: isSelected ? selectedLabelColor : labelColor,
217
+ },
218
+ ]}
219
+ >
220
+ {option}
221
+ </Text>
222
+
223
+ {/* Description */}
224
+ {description ? (
225
+ <Text
226
+ style={[
227
+ styles.description,
228
+ {
229
+ fontSize: descriptionFontSize,
230
+ color: descriptionColor,
231
+ },
232
+ ]}
233
+ >
234
+ {description}
235
+ </Text>
236
+ ) : null}
237
+ </TouchableOpacity>
238
+ );
239
+ })}
240
+ </View>
241
+ </View>
242
+ );
243
+ };
244
+
245
+ const styles = StyleSheet.create({
246
+ container: {
247
+ width: '100%',
248
+ },
249
+ grid: {
250
+ flexDirection: 'row',
251
+ flexWrap: 'wrap',
252
+ },
253
+ card: {
254
+ position: 'relative',
255
+ borderWidth: 2,
256
+ padding: 16,
257
+ justifyContent: 'center',
258
+ },
259
+ label: {
260
+ marginTop: 4,
261
+ },
262
+ description: {
263
+ marginTop: 4,
264
+ },
265
+ });