@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,496 @@
1
+ /**
2
+ * TabsComponent - Interactive tabs for React Native
3
+ * Uses element:{id}.currentIndex for state tracking (Superwall pattern)
4
+ * Full property support matching web schema for cross-platform consistency
5
+ * Phase 2 Enhanced: icon support, alignment, sizing, disabled state, animation duration, overflow
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
9
+ import {
10
+ View,
11
+ Text,
12
+ TouchableOpacity,
13
+ StyleSheet,
14
+ LayoutAnimation,
15
+ Platform,
16
+ UIManager,
17
+ ViewStyle,
18
+ TextStyle,
19
+ ScrollView,
20
+ } from 'react-native';
21
+ import type { PaywallComponent } from '../types';
22
+
23
+ // Enable LayoutAnimation on Android
24
+ if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
25
+ UIManager.setLayoutAnimationEnabledExperimental(true);
26
+ }
27
+
28
+ // Icon mapping for React Native (using basic Unicode symbols as fallback)
29
+ const ICON_MAP: Record<string, string> = {
30
+ home: '🏠',
31
+ star: '⭐',
32
+ settings: '⚙️',
33
+ person: '👤',
34
+ favorite: '❤️',
35
+ heart: '❤️',
36
+ search: '🔍',
37
+ check: '✓',
38
+ close: '✕',
39
+ add: '+',
40
+ remove: '-',
41
+ edit: '✏️',
42
+ delete: '🗑️',
43
+ share: '↗️',
44
+ download: '⬇️',
45
+ upload: '⬆️',
46
+ refresh: '🔄',
47
+ info: 'ℹ️',
48
+ help: '❓',
49
+ warning: '⚠️',
50
+ error: '❌',
51
+ lock: '🔒',
52
+ unlock: '🔓',
53
+ notification: '🔔',
54
+ email: '✉️',
55
+ phone: '📞',
56
+ location: '📍',
57
+ calendar: '📅',
58
+ time: '⏰',
59
+ camera: '📷',
60
+ photo: '🖼️',
61
+ video: '🎥',
62
+ music: '🎵',
63
+ play: '▶️',
64
+ pause: '⏸️',
65
+ stop: '⏹️',
66
+ crown: '👑',
67
+ premium: '👑',
68
+ diamond: '💎',
69
+ gift: '🎁',
70
+ cart: '🛒',
71
+ wallet: '💰',
72
+ rocket: '🚀',
73
+ trophy: '🏆',
74
+ medal: '🏅',
75
+ badge: '📛',
76
+ verified: '✅',
77
+ shield: '🛡️',
78
+ bolt: '⚡',
79
+ fire: '🔥',
80
+ sparkle: '✨',
81
+ magic: '🪄',
82
+ };
83
+
84
+ interface TabsComponentProps {
85
+ component: PaywallComponent;
86
+ /** Child components to render within the tabs (defaults to component.children) */
87
+ children?: PaywallComponent[];
88
+ renderChild: (child: PaywallComponent, index: number) => React.ReactNode;
89
+ stateManager?: any;
90
+ isPreviewMode?: boolean;
91
+ onTabChange?: (componentId: string, previousIndex: number, newIndex: number, tabLabel: string) => void;
92
+ }
93
+
94
+ export const TabsComponent: React.FC<TabsComponentProps> = ({
95
+ component,
96
+ renderChild,
97
+ stateManager,
98
+ isPreviewMode = false,
99
+ onTabChange,
100
+ }) => {
101
+ const props = component.properties || {};
102
+
103
+ // Core Properties
104
+ const tabCount = (props.tabCount as number) ?? 3;
105
+ const defaultTab = (props.defaultTab as number) ?? 0;
106
+ const animated = (props.animated as boolean) ?? true;
107
+ const variant = (props.variant as string) ?? 'pills';
108
+
109
+ // Phase 2: New Properties
110
+ const tabAlignment = (props.tabAlignment as string) ?? 'stretch';
111
+ const tabSizing = (props.tabSizing as string) ?? 'equal';
112
+ const fixedTabWidth = props.fixedTabWidth as number | undefined;
113
+ const animationDuration = (props.animationDuration as number) ?? 200;
114
+ const overflowBehavior = (props.overflowBehavior as string) ?? 'scroll';
115
+ const tabIconPosition = (props.tabIconPosition as string) ?? 'left';
116
+
117
+ // Styling Properties (matching web schema)
118
+ const activeTabColor = (props.activeTabColor as string) ?? '#FFFFFF';
119
+ const inactiveTabColor = (props.inactiveTabColor as string) ?? 'transparent';
120
+ const activeTextColor = (props.activeTextColor as string) ?? '#000000';
121
+ const inactiveTextColor = (props.inactiveTextColor as string) ?? '#666666';
122
+ const tabListBackgroundColor = (props.tabListBackgroundColor as string) ?? '#F5F5F5';
123
+ const tabFontSize = (props.tabFontSize as number) ?? 14;
124
+ const tabFontWeight = (props.tabFontWeight as string) ?? '600';
125
+ const tabBorderRadius = (props.tabBorderRadius as number) ?? 8;
126
+ const tabListBorderRadius = (props.tabListBorderRadius as number) ?? 10;
127
+ const tabGap = (props.tabGap as number) ?? 4;
128
+ const tabPaddingX = (props.tabPaddingX as number) ?? 16;
129
+ const tabPaddingY = (props.tabPaddingY as number) ?? 10;
130
+
131
+ // Phase 2: Disabled state colors
132
+ const disabledTextColor = (props.disabledTextColor as string) ?? '#AAAAAA';
133
+ const disabledTabFill = (props.disabledTabFill as string) ?? '#F0F0F0';
134
+
135
+ const [currentIndex, setCurrentIndex] = useState(defaultTab);
136
+
137
+ // Resolve tab children by role (canonical pattern matching all other renderers)
138
+ const tabChildren = useMemo(() => {
139
+ return (component.children || [])
140
+ .filter(child => /^tab-\d+$/.test(child.role || ''))
141
+ .sort((a, b) => {
142
+ const aNum = parseInt(a.role?.replace('tab-', '') || '0');
143
+ const bNum = parseInt(b.role?.replace('tab-', '') || '0');
144
+ return aNum - bNum;
145
+ });
146
+ }, [component.children]);
147
+
148
+ // Get tab label from child properties (canonical: label → text → name)
149
+ const getTabLabel = useCallback((index: number): string => {
150
+ const child = tabChildren[index];
151
+ if (!child) return `Tab ${index + 1}`;
152
+ return (child.properties?.label as string)
153
+ ?? (child.properties?.text as string)
154
+ ?? (child.name as string)
155
+ ?? `Tab ${index + 1}`;
156
+ }, [tabChildren]);
157
+
158
+ // Get tab icon from child properties
159
+ const getTabIcon = useCallback((index: number): string | undefined => {
160
+ const child = tabChildren[index];
161
+ const iconName = child?.properties?.icon as string | undefined;
162
+ if (iconName && ICON_MAP[iconName.toLowerCase()]) {
163
+ return ICON_MAP[iconName.toLowerCase()];
164
+ }
165
+ return undefined;
166
+ }, [tabChildren]);
167
+
168
+ // Check if tab is disabled from child properties
169
+ const isTabDisabled = useCallback((index: number): boolean => {
170
+ const child = tabChildren[index];
171
+ return (child?.properties?.disabled as boolean) ?? false;
172
+ }, [tabChildren]);
173
+
174
+ // Parse children to find content panels - filter by content-panel-X role and sort
175
+ const contentPanels = useMemo(() => {
176
+ const children = component.children || [];
177
+ return children
178
+ .filter(child => /^content-panel-\d+$/.test(child.role || ''))
179
+ .sort((a, b) => {
180
+ const aNum = parseInt(a.role?.replace('content-panel-', '') || '0');
181
+ const bNum = parseInt(b.role?.replace('content-panel-', '') || '0');
182
+ return aNum - bNum;
183
+ });
184
+ }, [component.children]);
185
+
186
+ // Update state manager
187
+ useEffect(() => {
188
+ if (stateManager) {
189
+ stateManager.updateState(`element:${component.id}.currentIndex`, currentIndex);
190
+ }
191
+ }, [component.id, currentIndex, stateManager]);
192
+
193
+ // Listen for external state changes
194
+ useEffect(() => {
195
+ if (!stateManager) return;
196
+
197
+ const unsubscribe = stateManager.subscribeToState(
198
+ `element:${component.id}.currentIndex`,
199
+ (value: number) => {
200
+ if (typeof value === 'number' && value !== currentIndex) {
201
+ selectTab(value);
202
+ }
203
+ }
204
+ );
205
+
206
+ return unsubscribe;
207
+ }, [component.id, currentIndex, stateManager]);
208
+
209
+ const selectTab = useCallback((index: number) => {
210
+ if (index < 0 || index >= tabCount) return;
211
+
212
+ // Phase 2: Check if tab is disabled
213
+ if (isTabDisabled(index)) return;
214
+
215
+ const previousIndex = currentIndex;
216
+ const tabLabel = getTabLabel(index);
217
+
218
+ if (animated) {
219
+ LayoutAnimation.configureNext({
220
+ duration: animationDuration,
221
+ update: { type: LayoutAnimation.Types.easeInEaseOut },
222
+ });
223
+ }
224
+ setCurrentIndex(index);
225
+
226
+ // Phase 2: Fire tab change callback
227
+ onTabChange?.(component.id, previousIndex, index, tabLabel);
228
+ }, [animated, animationDuration, tabCount, currentIndex, isTabDisabled, getTabLabel, onTabChange, component.id]);
229
+
230
+ // Content gap from containerLayout (matching web schema)
231
+ const contentGap = (component.containerLayout?.gap as number) ?? 12;
232
+
233
+ // Phase 2: Get flex justification based on alignment
234
+ const getJustifyContent = (): ViewStyle['justifyContent'] => {
235
+ switch (tabAlignment) {
236
+ case 'start': return 'flex-start';
237
+ case 'center': return 'center';
238
+ case 'end': return 'flex-end';
239
+ case 'stretch':
240
+ default: return 'flex-start';
241
+ }
242
+ };
243
+
244
+ // Variant-specific styles
245
+ const getTabListStyle = (): ViewStyle => {
246
+ const baseStyle: ViewStyle = {
247
+ backgroundColor: tabListBackgroundColor,
248
+ flexDirection: 'row',
249
+ padding: 4,
250
+ marginBottom: contentGap,
251
+ justifyContent: getJustifyContent(),
252
+ };
253
+
254
+ switch (variant) {
255
+ case 'underline':
256
+ return {
257
+ ...baseStyle,
258
+ backgroundColor: 'transparent',
259
+ borderBottomWidth: 1,
260
+ borderBottomColor: '#e5e5e5',
261
+ padding: 0,
262
+ };
263
+ case 'boxed':
264
+ return {
265
+ ...baseStyle,
266
+ borderRadius: tabBorderRadius,
267
+ borderWidth: 1,
268
+ borderColor: '#e5e5e5',
269
+ padding: 0,
270
+ };
271
+ default: // pills
272
+ return {
273
+ ...baseStyle,
274
+ borderRadius: tabListBorderRadius,
275
+ };
276
+ }
277
+ };
278
+
279
+ const getTabButtonStyle = (isActive: boolean, isDisabled: boolean): ViewStyle => {
280
+ // Phase 2: Determine width based on sizing mode
281
+ let widthStyle: ViewStyle = {};
282
+ if (tabSizing === 'equal' && tabAlignment === 'stretch') {
283
+ widthStyle = { flex: 1 };
284
+ } else if (tabSizing === 'fixed' && fixedTabWidth) {
285
+ widthStyle = { width: fixedTabWidth };
286
+ }
287
+
288
+ // Phase 2: Determine background color
289
+ const bgColor = isDisabled
290
+ ? disabledTabFill
291
+ : (isActive ? activeTabColor : inactiveTabColor);
292
+
293
+ const baseStyle: ViewStyle = {
294
+ ...widthStyle,
295
+ paddingVertical: tabPaddingY,
296
+ paddingHorizontal: tabPaddingX,
297
+ alignItems: 'center',
298
+ justifyContent: 'center',
299
+ marginHorizontal: tabGap / 2,
300
+ opacity: isDisabled ? 0.5 : 1,
301
+ };
302
+
303
+ switch (variant) {
304
+ case 'underline':
305
+ return {
306
+ ...baseStyle,
307
+ marginHorizontal: 0,
308
+ borderBottomWidth: isActive ? 2 : 0,
309
+ borderBottomColor: isActive ? activeTextColor : 'transparent',
310
+ };
311
+ case 'boxed':
312
+ return {
313
+ ...baseStyle,
314
+ marginHorizontal: 0,
315
+ backgroundColor: bgColor,
316
+ };
317
+ default: // pills
318
+ return {
319
+ ...baseStyle,
320
+ backgroundColor: bgColor,
321
+ borderRadius: tabBorderRadius,
322
+ };
323
+ }
324
+ };
325
+
326
+ const getTabTextStyle = (isActive: boolean, isDisabled: boolean): TextStyle => {
327
+ const textColor = isDisabled
328
+ ? disabledTextColor
329
+ : (isActive ? activeTextColor : inactiveTextColor);
330
+
331
+ return {
332
+ fontSize: tabFontSize,
333
+ fontWeight: isActive ? (tabFontWeight as TextStyle['fontWeight']) : '500',
334
+ color: textColor,
335
+ };
336
+ };
337
+
338
+ // Phase 2: Render tab content with icon
339
+ const renderTabContent = (index: number, isActive: boolean, isDisabled: boolean) => {
340
+ const label = getTabLabel(index);
341
+ const icon = getTabIcon(index);
342
+ const textStyle = getTabTextStyle(isActive, isDisabled);
343
+
344
+ if (!icon || tabIconPosition === 'only') {
345
+ if (icon && tabIconPosition === 'only') {
346
+ return <Text style={[textStyle, { fontSize: tabFontSize + 4 }]}>{icon}</Text>;
347
+ }
348
+ return <Text style={textStyle}>{label}</Text>;
349
+ }
350
+
351
+ const iconElement = <Text style={[textStyle, { fontSize: tabFontSize + 2, marginHorizontal: 4 }]}>{icon}</Text>;
352
+ const labelElement = <Text style={textStyle}>{label}</Text>;
353
+
354
+ switch (tabIconPosition) {
355
+ case 'right':
356
+ return (
357
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
358
+ {labelElement}
359
+ {iconElement}
360
+ </View>
361
+ );
362
+ case 'top':
363
+ return (
364
+ <View style={{ alignItems: 'center' }}>
365
+ {iconElement}
366
+ {labelElement}
367
+ </View>
368
+ );
369
+ case 'left':
370
+ default:
371
+ return (
372
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
373
+ {iconElement}
374
+ {labelElement}
375
+ </View>
376
+ );
377
+ }
378
+ };
379
+
380
+ // Render tab list
381
+ const renderTabList = () => {
382
+ const actualTabCount = tabChildren.length || tabCount;
383
+ const tabButtons = Array.from({ length: actualTabCount }, (_, index) => {
384
+ const isActive = index === currentIndex;
385
+ const disabled = isTabDisabled(index);
386
+
387
+ return (
388
+ <React.Fragment key={index}>
389
+ <TouchableOpacity
390
+ style={getTabButtonStyle(isActive, disabled)}
391
+ onPress={() => selectTab(index)}
392
+ activeOpacity={disabled ? 1 : 0.7}
393
+ disabled={disabled}
394
+ >
395
+ {renderTabContent(index, isActive, disabled)}
396
+ </TouchableOpacity>
397
+ {variant === 'boxed' && index < actualTabCount - 1 && (
398
+ <View style={styles.boxedDivider} />
399
+ )}
400
+ </React.Fragment>
401
+ );
402
+ });
403
+
404
+ const tabListStyle = getTabListStyle();
405
+
406
+ // Phase 2: Handle overflow behavior
407
+ if (overflowBehavior === 'scroll') {
408
+ return (
409
+ <ScrollView
410
+ horizontal
411
+ showsHorizontalScrollIndicator={false}
412
+ contentContainerStyle={tabListStyle}
413
+ >
414
+ {tabButtons}
415
+ </ScrollView>
416
+ );
417
+ }
418
+
419
+ return (
420
+ <View style={tabListStyle}>
421
+ {tabButtons}
422
+ </View>
423
+ );
424
+ };
425
+
426
+ // Render content panel - no fallback to first panel
427
+ const renderContentPanel = () => {
428
+ const panel = contentPanels[currentIndex];
429
+
430
+ if (!panel) {
431
+ // Empty placeholder matching web style
432
+ return (
433
+ <View style={styles.emptyPlaceholder}>
434
+ <Text style={styles.emptyPlaceholderText}>
435
+ No content panel for Tab {currentIndex + 1}
436
+ </Text>
437
+ </View>
438
+ );
439
+ }
440
+
441
+ return (
442
+ <View style={styles.contentPanel}>
443
+ {renderChild(panel, currentIndex)}
444
+ </View>
445
+ );
446
+ };
447
+
448
+ if (!component.children?.length) {
449
+ return (
450
+ <View style={styles.emptyContainer}>
451
+ <Text style={styles.emptyText}>Add tabs to the component</Text>
452
+ </View>
453
+ );
454
+ }
455
+
456
+ return (
457
+ <View style={styles.container}>
458
+ {renderTabList()}
459
+ {renderContentPanel()}
460
+ </View>
461
+ );
462
+ };
463
+
464
+ const styles = StyleSheet.create({
465
+ container: {
466
+ flex: 1,
467
+ },
468
+ contentPanel: {
469
+ flex: 1,
470
+ },
471
+ boxedDivider: {
472
+ width: 1,
473
+ backgroundColor: '#e5e5e5',
474
+ },
475
+ emptyContainer: {
476
+ padding: 32,
477
+ alignItems: 'center',
478
+ justifyContent: 'center',
479
+ },
480
+ emptyText: {
481
+ color: '#999',
482
+ fontSize: 14,
483
+ },
484
+ emptyPlaceholder: {
485
+ minHeight: 100,
486
+ backgroundColor: 'rgba(128, 128, 128, 0.1)',
487
+ borderRadius: 8,
488
+ padding: 16,
489
+ alignItems: 'center',
490
+ justifyContent: 'center',
491
+ },
492
+ emptyPlaceholderText: {
493
+ color: 'rgba(128, 128, 128, 0.6)',
494
+ fontSize: 14,
495
+ },
496
+ });