@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,294 @@
1
+ /**
2
+ * NPSScaleComponent - React Native
3
+ *
4
+ * Net Promoter Score scale with child-based architecture.
5
+ * Renders children with nps-button-* roles, applies selectedAppearance for selected state.
6
+ */
7
+
8
+ import React, { useState, useCallback, useEffect, useMemo } from 'react';
9
+ import {
10
+ View,
11
+ Text,
12
+ TouchableOpacity,
13
+ StyleSheet,
14
+ Animated,
15
+ } from 'react-native';
16
+ import { PaywallComponent } from '../types';
17
+
18
+ interface NPSScaleComponentProps {
19
+ component: PaywallComponent;
20
+ stateManager?: any;
21
+ isPreviewMode?: boolean;
22
+ renderChild?: (child: PaywallComponent) => React.ReactNode;
23
+ }
24
+
25
+ // NPS Categories
26
+ const getNPSCategory = (value: number, min: number, max: number): string => {
27
+ const normalizedMax = max - min;
28
+ const normalizedValue = value - min;
29
+ const percentage = normalizedValue / normalizedMax;
30
+
31
+ if (percentage <= 0.6) return 'detractor';
32
+ if (percentage <= 0.8) return 'passive';
33
+ return 'promoter';
34
+ };
35
+
36
+ // Extract value from role like "nps-button-5"
37
+ const extractValueFromRole = (role?: string): number => {
38
+ if (!role || !role.startsWith('nps-button-')) return 0;
39
+ const valueStr = role.replace('nps-button-', '');
40
+ return parseInt(valueStr, 10) || 0;
41
+ };
42
+
43
+ export const NPSScaleComponent: React.FC<NPSScaleComponentProps> = ({
44
+ component,
45
+ stateManager,
46
+ isPreviewMode = false,
47
+ renderChild,
48
+ }) => {
49
+ const props = component.properties as any;
50
+ const componentId = component.id;
51
+
52
+ // Configuration
53
+ const minValue = props.minValue ?? 0;
54
+ const maxValue = props.maxValue ?? 10;
55
+ const defaultValue = props.defaultValue ?? -1;
56
+ const showLabels = props.showLabels !== false;
57
+ const minLabel = props.minLabel || 'Not at all likely';
58
+ const maxLabel = props.maxLabel || 'Extremely likely';
59
+ const buttonGap = props.buttonGap || 4;
60
+
61
+ const [selectedValue, setSelectedValue] = useState<number>(defaultValue);
62
+ const [animatedValues] = useState(() =>
63
+ Array.from({ length: maxValue - minValue + 1 }, () => new Animated.Value(1))
64
+ );
65
+
66
+ // Get NPS button children sorted by value
67
+ const npsButtonChildren = useMemo(() => {
68
+ const children = component.children || [];
69
+ return children
70
+ .filter((child: PaywallComponent) => child.role?.startsWith('nps-button-'))
71
+ .sort((a: PaywallComponent, b: PaywallComponent) => {
72
+ const valueA = extractValueFromRole(a.role);
73
+ const valueB = extractValueFromRole(b.role);
74
+ return valueA - valueB;
75
+ });
76
+ }, [component.children]);
77
+
78
+ const hasChildren = npsButtonChildren.length > 0 && renderChild;
79
+
80
+ // Update state manager when value changes
81
+ useEffect(() => {
82
+ if (stateManager && selectedValue >= minValue) {
83
+ const category = getNPSCategory(selectedValue, minValue, maxValue);
84
+
85
+ stateManager.updateState(`element:${componentId}.score`, selectedValue);
86
+ stateManager.updateState(`element:${componentId}.category`, category);
87
+ stateManager.updateState(`element:${componentId}.isValid`, true);
88
+ }
89
+ }, [selectedValue, componentId, minValue, maxValue, stateManager]);
90
+
91
+ // Subscribe to external state changes
92
+ useEffect(() => {
93
+ if (stateManager) {
94
+ return stateManager.subscribeToState(
95
+ `element:${componentId}.score`,
96
+ (value: number) => {
97
+ if (value !== selectedValue) {
98
+ setSelectedValue(value);
99
+ }
100
+ }
101
+ );
102
+ }
103
+ }, [componentId, stateManager, selectedValue]);
104
+
105
+ // Handle value selection
106
+ const handleSelect = useCallback((value: number, index: number) => {
107
+ // Animate button
108
+ Animated.sequence([
109
+ Animated.timing(animatedValues[index], {
110
+ toValue: 0.9,
111
+ duration: 50,
112
+ useNativeDriver: true,
113
+ }),
114
+ Animated.timing(animatedValues[index], {
115
+ toValue: 1.1,
116
+ duration: 100,
117
+ useNativeDriver: true,
118
+ }),
119
+ Animated.timing(animatedValues[index], {
120
+ toValue: 1,
121
+ duration: 100,
122
+ useNativeDriver: true,
123
+ }),
124
+ ]).start();
125
+
126
+ setSelectedValue(value);
127
+ }, [animatedValues]);
128
+
129
+ // Apply selectedAppearance to child when selected
130
+ const applySelectedAppearance = useCallback((child: PaywallComponent, isSelected: boolean): PaywallComponent => {
131
+ if (!isSelected || !child.selectedAppearance) {
132
+ return child;
133
+ }
134
+
135
+ // Create a copy with selectedAppearance merged into appearance
136
+ return {
137
+ ...child,
138
+ appearance: child.selectedAppearance,
139
+ };
140
+ }, []);
141
+
142
+ // Get styling from component.properties for fallback rendering (canonical direct properties)
143
+ const getSelectionStyles = useCallback(() => {
144
+ const componentAppearance = (component as any).appearance || {};
145
+
146
+ // Read from component.properties (canonical names per Property Parity Contract)
147
+ const buttonFill = props.buttonFill;
148
+ const selectedFill = props.selectedFill;
149
+
150
+ const buttonSize = props.buttonSize ?? 40;
151
+ const borderRadius = componentAppearance.cornerRadius ?? props.borderRadius ?? 8;
152
+ const fontSize = props.labelFontSize ?? 14;
153
+
154
+ // Extract colors from FillValue objects
155
+ const activeBackground = selectedFill?.resolvedColor ?? selectedFill?.color ?? '#3B82F6';
156
+ const inactiveBackground = buttonFill?.resolvedColor ?? buttonFill?.color ??
157
+ componentAppearance.fill?.resolvedColor ?? componentAppearance.fill?.color ?? '#E5E7EB';
158
+ const activeTextColor = props.selectedTextColor ?? '#FFFFFF';
159
+ const inactiveTextColor = props.textColor ?? '#6B7280';
160
+
161
+ return {
162
+ buttonSize,
163
+ borderRadius,
164
+ fontSize,
165
+ activeBackground,
166
+ inactiveBackground,
167
+ activeTextColor,
168
+ inactiveTextColor,
169
+ };
170
+ }, [component, props]);
171
+
172
+ const selectionStyles = getSelectionStyles();
173
+
174
+ // Render child-based buttons
175
+ const renderChildBasedButtons = () => {
176
+ return npsButtonChildren.map((child: PaywallComponent, index: number) => {
177
+ const value = extractValueFromRole(child.role);
178
+ const isSelected = selectedValue === value;
179
+
180
+ return (
181
+ <TouchableOpacity
182
+ key={child.id}
183
+ onPress={() => handleSelect(value, index)}
184
+ activeOpacity={0.7}
185
+ style={{ marginHorizontal: buttonGap / 2 }}
186
+ >
187
+ <Animated.View
188
+ style={{
189
+ transform: [{ scale: animatedValues[index] }],
190
+ }}
191
+ >
192
+ {renderChild!(applySelectedAppearance(child, isSelected))}
193
+ </Animated.View>
194
+ </TouchableOpacity>
195
+ );
196
+ });
197
+ };
198
+
199
+ // Render fallback buttons when no children available
200
+ const renderFallbackButtons = () => {
201
+ const values: number[] = [];
202
+ for (let i = minValue; i <= maxValue; i++) {
203
+ values.push(i);
204
+ }
205
+
206
+ return values.map((value, index) => {
207
+ const isSelected = selectedValue === value;
208
+ const backgroundColor = isSelected
209
+ ? selectionStyles.activeBackground
210
+ : selectionStyles.inactiveBackground;
211
+ const textColor = isSelected
212
+ ? selectionStyles.activeTextColor
213
+ : selectionStyles.inactiveTextColor;
214
+
215
+ return (
216
+ <TouchableOpacity
217
+ key={value}
218
+ onPress={() => handleSelect(value, index)}
219
+ activeOpacity={0.7}
220
+ style={{ marginHorizontal: buttonGap / 2 }}
221
+ >
222
+ <Animated.View
223
+ style={[
224
+ styles.button,
225
+ {
226
+ width: selectionStyles.buttonSize,
227
+ height: selectionStyles.buttonSize,
228
+ borderRadius: selectionStyles.borderRadius,
229
+ backgroundColor,
230
+ transform: [{ scale: animatedValues[index] }],
231
+ },
232
+ ]}
233
+ >
234
+ <Text style={[
235
+ styles.buttonText,
236
+ {
237
+ fontSize: selectionStyles.fontSize,
238
+ color: textColor,
239
+ fontWeight: isSelected ? '600' : '500',
240
+ }
241
+ ]}>
242
+ {value}
243
+ </Text>
244
+ </Animated.View>
245
+ </TouchableOpacity>
246
+ );
247
+ });
248
+ };
249
+
250
+ return (
251
+ <View style={styles.container}>
252
+ {/* Scale Buttons */}
253
+ <View style={[styles.scaleContainer, { gap: buttonGap }]}>
254
+ {hasChildren ? renderChildBasedButtons() : renderFallbackButtons()}
255
+ </View>
256
+
257
+ {/* Labels */}
258
+ {showLabels && (
259
+ <View style={styles.labelsContainer}>
260
+ <Text style={styles.labelText}>{minLabel}</Text>
261
+ <Text style={styles.labelText}>{maxLabel}</Text>
262
+ </View>
263
+ )}
264
+ </View>
265
+ );
266
+ };
267
+
268
+ const styles = StyleSheet.create({
269
+ container: {
270
+ width: '100%',
271
+ gap: 16,
272
+ },
273
+ scaleContainer: {
274
+ flexDirection: 'row',
275
+ flexWrap: 'wrap',
276
+ justifyContent: 'center',
277
+ },
278
+ button: {
279
+ alignItems: 'center',
280
+ justifyContent: 'center',
281
+ },
282
+ buttonText: {
283
+ fontWeight: '500',
284
+ },
285
+ labelsContainer: {
286
+ flexDirection: 'row',
287
+ justifyContent: 'space-between',
288
+ paddingHorizontal: 4,
289
+ },
290
+ labelText: {
291
+ fontSize: 12,
292
+ color: '#6B7280',
293
+ },
294
+ });
@@ -0,0 +1,329 @@
1
+ /**
2
+ * NavigationComponent - React Native
3
+ *
4
+ * Multi-screen paywall navigation with state management.
5
+ * Features:
6
+ * - Page stack navigation
7
+ * - State integration for current page tracking
8
+ * - Transition animations (slide, fade, push, none)
9
+ * - Swipe gesture navigation
10
+ * - Forward/back navigation
11
+ * - Entry/exit condition support
12
+ */
13
+
14
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
15
+ import {
16
+ View,
17
+ Animated,
18
+ PanResponder,
19
+ Dimensions,
20
+ StyleSheet,
21
+ } from 'react-native';
22
+ import { PaywallComponent } from '../types';
23
+
24
+ interface NavigationComponentProps {
25
+ component: PaywallComponent;
26
+ /** Child components to render within the navigation (defaults to component.children) */
27
+ children?: PaywallComponent[];
28
+ stateManager?: any;
29
+ isPreviewMode?: boolean;
30
+ renderComponent?: (component: PaywallComponent) => React.ReactNode;
31
+ /** Alias for renderComponent used by ComponentRenderer */
32
+ renderChild?: (child: PaywallComponent, index?: number) => React.ReactNode;
33
+ }
34
+
35
+ type TransitionType = 'slide' | 'fade' | 'push' | 'none';
36
+ type TransitionDirection = 'horizontal' | 'vertical';
37
+
38
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
39
+
40
+ export const NavigationComponent: React.FC<NavigationComponentProps> = ({
41
+ component,
42
+ stateManager,
43
+ isPreviewMode = false,
44
+ renderComponent,
45
+ }) => {
46
+ const props = component.properties as any;
47
+ const componentId = component.id;
48
+
49
+ // Configuration
50
+ const transition: TransitionType = props.transition || 'slide';
51
+ const transitionDirection: TransitionDirection = props.transitionDirection || 'horizontal';
52
+ const transitionDuration = props.transitionDuration || 300;
53
+ const swipeEnabled = props.swipeEnabled !== false;
54
+ const swipeThreshold = props.swipeThreshold || 0.3;
55
+ const initialPage = props.initialPage || 0;
56
+
57
+ // Get pages from children
58
+ const pages = component.children || [];
59
+ const totalPages = pages.length;
60
+
61
+ // State
62
+ const [currentIndex, setCurrentIndex] = useState(initialPage);
63
+ const [history, setHistory] = useState<number[]>([initialPage]);
64
+ const animatedValue = useRef(new Animated.Value(0)).current;
65
+ const panValue = useRef(new Animated.Value(0)).current;
66
+
67
+ // State manager integration
68
+ useEffect(() => {
69
+ if (stateManager) {
70
+ // Initialize state
71
+ stateManager.updateState(`element:${componentId}.currentIndex`, currentIndex);
72
+ stateManager.updateState(`element:${componentId}.totalPages`, totalPages);
73
+ stateManager.updateState(`element:${componentId}.canGoBack`, history.length > 1);
74
+ stateManager.updateState(`element:${componentId}.history`, history);
75
+
76
+ // Subscribe to external navigation commands
77
+ const unsubscribe = stateManager.subscribeToState(
78
+ `element:${componentId}.navigateTo`,
79
+ (value: number | null) => {
80
+ if (value !== null && value >= 0 && value < totalPages && value !== currentIndex) {
81
+ navigateToPage(value);
82
+ }
83
+ }
84
+ );
85
+
86
+ return unsubscribe;
87
+ }
88
+ }, [componentId, stateManager, currentIndex, totalPages, history]);
89
+
90
+ // Update state manager when current index changes
91
+ useEffect(() => {
92
+ if (stateManager) {
93
+ stateManager.updateState(`element:${componentId}.currentIndex`, currentIndex);
94
+ stateManager.updateState(`element:${componentId}.canGoBack`, history.length > 1);
95
+ stateManager.updateState(`element:${componentId}.history`, history);
96
+ }
97
+ }, [currentIndex, history, componentId, stateManager]);
98
+
99
+ // Navigate to a specific page
100
+ const navigateToPage = useCallback((pageIndex: number, options?: { replace?: boolean }) => {
101
+ if (pageIndex < 0 || pageIndex >= totalPages || pageIndex === currentIndex) return;
102
+
103
+ const direction = pageIndex > currentIndex ? 1 : -1;
104
+
105
+ // Animate transition
106
+ Animated.timing(animatedValue, {
107
+ toValue: direction,
108
+ duration: transitionDuration,
109
+ useNativeDriver: true,
110
+ }).start(() => {
111
+ setCurrentIndex(pageIndex);
112
+
113
+ // Update history
114
+ if (options?.replace) {
115
+ setHistory(prev => [...prev.slice(0, -1), pageIndex]);
116
+ } else {
117
+ setHistory(prev => [...prev, pageIndex]);
118
+ }
119
+
120
+ animatedValue.setValue(0);
121
+ });
122
+ }, [currentIndex, totalPages, transitionDuration, animatedValue]);
123
+
124
+ // Go back in history
125
+ const goBack = useCallback(() => {
126
+ if (history.length <= 1) return;
127
+
128
+ const newHistory = [...history];
129
+ newHistory.pop();
130
+ const previousPage = newHistory[newHistory.length - 1];
131
+
132
+ Animated.timing(animatedValue, {
133
+ toValue: -1,
134
+ duration: transitionDuration,
135
+ useNativeDriver: true,
136
+ }).start(() => {
137
+ setCurrentIndex(previousPage);
138
+ setHistory(newHistory);
139
+ animatedValue.setValue(0);
140
+ });
141
+
142
+ if (stateManager) {
143
+ stateManager.updateState(`element:${componentId}.currentIndex`, previousPage);
144
+ }
145
+ }, [history, transitionDuration, animatedValue, componentId, stateManager]);
146
+
147
+ // Go to next page
148
+ const goNext = useCallback(() => {
149
+ if (currentIndex < totalPages - 1) {
150
+ navigateToPage(currentIndex + 1);
151
+ }
152
+ }, [currentIndex, totalPages, navigateToPage]);
153
+
154
+ // Go to previous page
155
+ const goPrevious = useCallback(() => {
156
+ if (currentIndex > 0) {
157
+ navigateToPage(currentIndex - 1);
158
+ }
159
+ }, [currentIndex, navigateToPage]);
160
+
161
+ // Pan responder for swipe navigation
162
+ const panResponder = useRef(
163
+ PanResponder.create({
164
+ onStartShouldSetPanResponder: () => false,
165
+ onMoveShouldSetPanResponder: (_, gestureState) => {
166
+ if (!swipeEnabled) return false;
167
+
168
+ const { dx, dy } = gestureState;
169
+ const isHorizontal = transitionDirection === 'horizontal';
170
+
171
+ if (isHorizontal) {
172
+ return Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10;
173
+ } else {
174
+ return Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > 10;
175
+ }
176
+ },
177
+ onPanResponderMove: (_, gestureState) => {
178
+ const { dx, dy } = gestureState;
179
+ const isHorizontal = transitionDirection === 'horizontal';
180
+ const delta = isHorizontal ? dx : dy;
181
+ const size = isHorizontal ? SCREEN_WIDTH : SCREEN_HEIGHT;
182
+
183
+ // Normalize pan value between -1 and 1
184
+ const normalizedDelta = delta / size;
185
+
186
+ // Check boundaries
187
+ if (normalizedDelta > 0 && currentIndex === 0) {
188
+ panValue.setValue(normalizedDelta * 0.2); // Resistance at start
189
+ } else if (normalizedDelta < 0 && currentIndex === totalPages - 1) {
190
+ panValue.setValue(normalizedDelta * 0.2); // Resistance at end
191
+ } else {
192
+ panValue.setValue(normalizedDelta);
193
+ }
194
+ },
195
+ onPanResponderRelease: (_, gestureState) => {
196
+ const { dx, dy, vx, vy } = gestureState;
197
+ const isHorizontal = transitionDirection === 'horizontal';
198
+ const delta = isHorizontal ? dx : dy;
199
+ const velocity = isHorizontal ? vx : vy;
200
+ const size = isHorizontal ? SCREEN_WIDTH : SCREEN_HEIGHT;
201
+
202
+ const normalizedDelta = delta / size;
203
+
204
+ // Determine if we should navigate
205
+ if (Math.abs(normalizedDelta) > swipeThreshold || Math.abs(velocity) > 0.5) {
206
+ if (normalizedDelta > 0 && currentIndex > 0) {
207
+ // Swipe right/down - go back
208
+ Animated.timing(panValue, {
209
+ toValue: 1,
210
+ duration: 200,
211
+ useNativeDriver: true,
212
+ }).start(() => {
213
+ panValue.setValue(0);
214
+ goPrevious();
215
+ });
216
+ } else if (normalizedDelta < 0 && currentIndex < totalPages - 1) {
217
+ // Swipe left/up - go forward
218
+ Animated.timing(panValue, {
219
+ toValue: -1,
220
+ duration: 200,
221
+ useNativeDriver: true,
222
+ }).start(() => {
223
+ panValue.setValue(0);
224
+ goNext();
225
+ });
226
+ } else {
227
+ // Snap back
228
+ Animated.spring(panValue, {
229
+ toValue: 0,
230
+ useNativeDriver: true,
231
+ }).start();
232
+ }
233
+ } else {
234
+ // Snap back
235
+ Animated.spring(panValue, {
236
+ toValue: 0,
237
+ useNativeDriver: true,
238
+ }).start();
239
+ }
240
+ },
241
+ })
242
+ ).current;
243
+
244
+ // Calculate animated transform based on transition type
245
+ const getTransformStyle = (pageOffset: number) => {
246
+ const isHorizontal = transitionDirection === 'horizontal';
247
+ const size = isHorizontal ? SCREEN_WIDTH : SCREEN_HEIGHT;
248
+
249
+ const combinedOffset = Animated.add(
250
+ animatedValue,
251
+ panValue
252
+ );
253
+
254
+ switch (transition) {
255
+ case 'slide':
256
+ const translateX = isHorizontal
257
+ ? Animated.multiply(combinedOffset, new Animated.Value(-size)).interpolate({
258
+ inputRange: [-1, 0, 1],
259
+ outputRange: [size + pageOffset * size, pageOffset * size, -size + pageOffset * size],
260
+ })
261
+ : 0;
262
+ const translateY = !isHorizontal
263
+ ? Animated.multiply(combinedOffset, new Animated.Value(-size)).interpolate({
264
+ inputRange: [-1, 0, 1],
265
+ outputRange: [size + pageOffset * size, pageOffset * size, -size + pageOffset * size],
266
+ })
267
+ : 0;
268
+ return { transform: [{ translateX }, { translateY }] };
269
+
270
+ case 'fade':
271
+ return {
272
+ opacity: combinedOffset.interpolate({
273
+ inputRange: [-1, 0, 1],
274
+ outputRange: pageOffset === 0 ? [0, 1, 0] : [1, 0, 1],
275
+ }),
276
+ };
277
+
278
+ case 'push':
279
+ const pushTranslate = isHorizontal
280
+ ? combinedOffset.interpolate({
281
+ inputRange: [-1, 0, 1],
282
+ outputRange: [size * 0.3, pageOffset * size, -size * 0.3],
283
+ })
284
+ : combinedOffset.interpolate({
285
+ inputRange: [-1, 0, 1],
286
+ outputRange: [size * 0.3, pageOffset * size, -size * 0.3],
287
+ });
288
+ const scale = combinedOffset.interpolate({
289
+ inputRange: [-1, 0, 1],
290
+ outputRange: pageOffset === 0 ? [0.9, 1, 0.9] : [1, 0.9, 1],
291
+ });
292
+ return {
293
+ transform: [
294
+ isHorizontal ? { translateX: pushTranslate } : { translateY: pushTranslate },
295
+ { scale },
296
+ ],
297
+ };
298
+
299
+ case 'none':
300
+ default:
301
+ return {};
302
+ }
303
+ };
304
+
305
+ // Render current page
306
+ const currentPage = pages[currentIndex];
307
+
308
+ if (!currentPage) {
309
+ return null;
310
+ }
311
+
312
+ return (
313
+ <View style={styles.container} {...panResponder.panHandlers}>
314
+ <Animated.View style={[styles.page, getTransformStyle(0)]}>
315
+ {renderComponent?.(currentPage)}
316
+ </Animated.View>
317
+ </View>
318
+ );
319
+ };
320
+
321
+ const styles = StyleSheet.create({
322
+ container: {
323
+ flex: 1,
324
+ overflow: 'hidden',
325
+ },
326
+ page: {
327
+ ...StyleSheet.absoluteFillObject,
328
+ },
329
+ });