@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,328 @@
1
+ /**
2
+ * PaywallView - Main React Native component for rendering paywalls
3
+ *
4
+ * Credentials and API base URL are resolved from BillDogConfiguration.
5
+ * Pass an `apiKey` prop only for backward compatibility.
6
+ */
7
+
8
+ import React, { useState, useEffect, useMemo, useRef } from 'react';
9
+ import { ScrollView, View, StyleSheet, ActivityIndicator, Dimensions } from 'react-native';
10
+ import { PaywallConfiguration, ThemeConfig, PaywallComponent } from './types';
11
+ import { ComponentRenderer } from './ComponentRenderer';
12
+ import { paywallStateManager } from './PaywallStateManager';
13
+ import { usePaywall } from './hooks/usePaywall';
14
+ import { detectDeviceCategory } from './utils/deviceDetection';
15
+ import { ThemeColorResolver } from './LayoutEngine';
16
+ import { LocalizationResolver } from './LocalizationResolver';
17
+
18
+ interface PaywallViewProps {
19
+ /**
20
+ * Paywall identifier to fetch from API
21
+ */
22
+ paywallIdentifier?: string;
23
+
24
+ /**
25
+ * Direct configuration (skips API fetch)
26
+ */
27
+ configuration?: PaywallConfiguration;
28
+
29
+ /**
30
+ * @deprecated Use BillDogConfiguration.configure({ apiKey }) instead
31
+ */
32
+ apiKey?: string;
33
+
34
+ /**
35
+ * Called when user interacts with buttons
36
+ */
37
+ onButtonPress?: (componentId: string, action: string) => void;
38
+
39
+ /**
40
+ * Called when paywall is dismissed
41
+ */
42
+ onDismiss?: () => void;
43
+
44
+ /**
45
+ * Loading component (optional)
46
+ */
47
+ LoadingComponent?: React.ComponentType;
48
+
49
+ /**
50
+ * Error component (optional)
51
+ */
52
+ ErrorComponent?: React.ComponentType<{ error: Error }>;
53
+ }
54
+
55
+ export const PaywallView: React.FC<PaywallViewProps> = ({
56
+ paywallIdentifier,
57
+ configuration: providedConfiguration,
58
+ apiKey,
59
+ onButtonPress,
60
+ onDismiss,
61
+ LoadingComponent,
62
+ ErrorComponent,
63
+ }) => {
64
+ const [deviceCategory, setDeviceCategory] = useState(detectDeviceCategory());
65
+
66
+ // ── ELEMENT STATE ────────────────────────────────────────────────────────────
67
+ // Components have always called a `stateManager` prop (updateState /
68
+ // subscribeToState / getState / setState) but nothing ever supplied one, and every
69
+ // call site optional-chains — so the whole cross-component state protocol was a
70
+ // silent no-op. Supplying the shared manager here activates it for the tree.
71
+ //
72
+ // Reset DURING FIRST RENDER, not in an effect: React runs child effects BEFORE
73
+ // parent effects, so clearing on mount would wipe the subscriptions the children
74
+ // just registered and leave every component permanently deaf. (Inverted from the
75
+ // native SDKs, where the parent clears first and children subscribe after.) The ref
76
+ // guard keeps it once-per-presentation and StrictMode-safe.
77
+ const stateResetRef = useRef(false);
78
+ if (!stateResetRef.current) {
79
+ paywallStateManager.clearAll();
80
+ stateResetRef.current = true;
81
+ }
82
+ useEffect(() => () => paywallStateManager.clearAll(), []);
83
+
84
+ // Fetch paywall if identifier is provided
85
+ const { configuration, loading, error, refetch } = usePaywall({
86
+ paywallIdentifier,
87
+ apiKey,
88
+ skipFetch: !!providedConfiguration,
89
+ });
90
+
91
+ // Listen for orientation changes
92
+ useEffect(() => {
93
+ const subscription = Dimensions.addEventListener('change', () => {
94
+ const newCategory = detectDeviceCategory();
95
+ if (newCategory !== deviceCategory) {
96
+ setDeviceCategory(newCategory);
97
+ if (!providedConfiguration) {
98
+ refetch();
99
+ }
100
+ }
101
+ });
102
+
103
+ return () => subscription?.remove();
104
+ }, [deviceCategory, providedConfiguration]);
105
+
106
+ // Use provided configuration or fetched configuration
107
+ const activeConfiguration = providedConfiguration || configuration;
108
+
109
+ /**
110
+ * Unwrap nested configuration structure
111
+ */
112
+ const unwrappedConfig = useMemo(() => {
113
+ if (!activeConfiguration) return null;
114
+ const config = activeConfiguration as any;
115
+
116
+ if (config.configuration && typeof config.configuration === 'object') {
117
+ const deviceConfig = config.configuration[deviceCategory] ||
118
+ config.configuration['phone_portrait'] ||
119
+ Object.values(config.configuration)[0];
120
+ if (deviceConfig && typeof deviceConfig === 'object') {
121
+ return {
122
+ ...deviceConfig,
123
+ wallConfigTheme: config.wallConfig?.themeConfig,
124
+ };
125
+ }
126
+ }
127
+
128
+ if (config[deviceCategory]?.theme || config[deviceCategory]?.components) {
129
+ return config[deviceCategory];
130
+ }
131
+
132
+ return config;
133
+ }, [activeConfiguration, deviceCategory]);
134
+
135
+ /**
136
+ * Comprehensive theme resolution
137
+ */
138
+ const resolvedTheme = useMemo((): ThemeConfig | undefined => {
139
+ if (!unwrappedConfig) return undefined;
140
+
141
+ const config = unwrappedConfig as any;
142
+
143
+ if (config.theme?.resolvedColors) {
144
+ return {
145
+ ...config.theme,
146
+ themingMode: config.theme.themingMode || 'full',
147
+ };
148
+ }
149
+
150
+ if (config.wallConfigTheme?.resolvedColors) {
151
+ return config.wallConfigTheme;
152
+ }
153
+
154
+ if (config.structured?.themes) {
155
+ const structuredTheme = config.structured.themes[deviceCategory] ||
156
+ config.structured.themes['phone_portrait'];
157
+ if (structuredTheme?.resolvedColors) {
158
+ return structuredTheme;
159
+ }
160
+ }
161
+
162
+ if (config.theme?.colors) {
163
+ return {
164
+ primaryColor: config.theme.colors.primary,
165
+ secondaryColor: config.theme.colors.secondary,
166
+ backgroundColor: config.theme.colors.background,
167
+ textColor: config.theme.colors.foreground || config.theme.colors.text,
168
+ cardColor: config.theme.colors.card,
169
+ mutedColor: config.theme.colors.muted,
170
+ borderColor: config.theme.colors.border,
171
+ accentColor: config.theme.colors.accent,
172
+ resolvedColors: config.theme.colors,
173
+ themingMode: config.theme.themingMode || 'full',
174
+ };
175
+ }
176
+
177
+ return config.theme;
178
+ }, [unwrappedConfig, deviceCategory]);
179
+
180
+ // Initialize ThemeColorResolver with resolved theme colors
181
+ useEffect(() => {
182
+ if (resolvedTheme) {
183
+ const themeForResolver: Record<string, string> = {};
184
+
185
+ if (resolvedTheme.primaryColor) themeForResolver.primaryColor = resolvedTheme.primaryColor;
186
+ if (resolvedTheme.secondaryColor) themeForResolver.secondaryColor = resolvedTheme.secondaryColor;
187
+ if (resolvedTheme.backgroundColor) themeForResolver.backgroundColor = resolvedTheme.backgroundColor;
188
+ if (resolvedTheme.textColor) themeForResolver.textColor = resolvedTheme.textColor;
189
+ if (resolvedTheme.cardColor) themeForResolver.cardColor = resolvedTheme.cardColor;
190
+ if (resolvedTheme.mutedColor) themeForResolver.mutedColor = resolvedTheme.mutedColor;
191
+ if (resolvedTheme.borderColor) themeForResolver.borderColor = resolvedTheme.borderColor;
192
+ if (resolvedTheme.accentColor) themeForResolver.accentColor = resolvedTheme.accentColor;
193
+
194
+ if (resolvedTheme.resolvedColors) {
195
+ Object.entries(resolvedTheme.resolvedColors).forEach(([key, value]) => {
196
+ if (typeof value === 'string') {
197
+ themeForResolver[key] = value;
198
+ }
199
+ });
200
+ }
201
+
202
+ ThemeColorResolver.setTheme(themeForResolver);
203
+ console.log('[PaywallView] Theme initialized:', themeForResolver);
204
+ } else {
205
+ ThemeColorResolver.setTheme(null);
206
+ }
207
+ }, [resolvedTheme]);
208
+
209
+ if (loading && !activeConfiguration) {
210
+ if (LoadingComponent) {
211
+ return <LoadingComponent />;
212
+ }
213
+ return (
214
+ <View style={styles.centered}>
215
+ <ActivityIndicator size="large" color="#007AFF" />
216
+ </View>
217
+ );
218
+ }
219
+
220
+ if (error && !activeConfiguration) {
221
+ if (ErrorComponent) {
222
+ return <ErrorComponent error={error} />;
223
+ }
224
+ return <View style={styles.centered} />;
225
+ }
226
+
227
+ if (!activeConfiguration) {
228
+ return null;
229
+ }
230
+
231
+ const backgroundColor = resolvedTheme?.backgroundColor ||
232
+ resolvedTheme?.resolvedColors?.background ||
233
+ '#FFFFFF';
234
+
235
+ // Layout direction — RTL when the dashboard set an explicit
236
+ // `theme.direction: 'rtl'` override, or when the effective locale's
237
+ // language is right-to-left. Mirrors Android `PaywallScreen.kt:431-458`.
238
+ // Applied as a Yoga `direction` style so flex layout mirrors for the
239
+ // whole component tree (row reverses, start/end flip).
240
+ const layoutDirection: 'ltr' | 'rtl' = (() => {
241
+ const explicit = (unwrappedConfig as any)?.theme?.direction
242
+ ?? (resolvedTheme as any)?.direction;
243
+ if (explicit === 'rtl') return 'rtl';
244
+ if (explicit === 'ltr') return 'ltr';
245
+ const baseLocale = (activeConfiguration as any)?.baseLocale as string | undefined;
246
+ const effectiveLocale = LocalizationResolver.getEffectiveLocale();
247
+ // A non-base effective locale wins; otherwise fall back to baseLocale.
248
+ const localeForDirection =
249
+ effectiveLocale && effectiveLocale !== baseLocale
250
+ ? effectiveLocale
251
+ : (baseLocale || effectiveLocale);
252
+ return LocalizationResolver.isRtlLocale(localeForDirection) ? 'rtl' : 'ltr';
253
+ })();
254
+
255
+ const components: PaywallComponent[] = unwrappedConfig?.components || activeConfiguration?.components || [];
256
+
257
+ const absoluteComponents = components.filter((c: PaywallComponent) => c.absolutePosition);
258
+ const relativeComponents = components.filter((c: PaywallComponent) => !c.absolutePosition);
259
+
260
+ const screenLayout = unwrappedConfig?.theme?.screenLayout;
261
+ const contentPadding = screenLayout?.padding || { top: 16, right: 16, bottom: 16, left: 16 };
262
+
263
+ return (
264
+ <View style={[styles.container, { backgroundColor, direction: layoutDirection }]}>
265
+ <ScrollView
266
+ style={styles.scrollView}
267
+ contentContainerStyle={[
268
+ styles.content,
269
+ {
270
+ paddingTop: contentPadding.top,
271
+ paddingRight: contentPadding.right,
272
+ paddingBottom: contentPadding.bottom,
273
+ paddingLeft: contentPadding.left,
274
+ gap: screenLayout?.gap || 16,
275
+ }
276
+ ]}
277
+ >
278
+ {relativeComponents.map((component) => (
279
+ <ComponentRenderer
280
+ key={component.id}
281
+ stateManager={paywallStateManager}
282
+ component={component}
283
+ configuration={activeConfiguration}
284
+ theme={resolvedTheme}
285
+ onButtonPress={onButtonPress}
286
+ />
287
+ ))}
288
+ </ScrollView>
289
+
290
+ {absoluteComponents.length > 0 && (
291
+ <View style={styles.absoluteContainer} pointerEvents="box-none">
292
+ {absoluteComponents.map((component) => (
293
+ <ComponentRenderer
294
+ key={component.id}
295
+ stateManager={paywallStateManager}
296
+ component={component}
297
+ configuration={activeConfiguration}
298
+ theme={resolvedTheme}
299
+ onButtonPress={onButtonPress}
300
+ />
301
+ ))}
302
+ </View>
303
+ )}
304
+ </View>
305
+ );
306
+ };
307
+
308
+ const styles = StyleSheet.create({
309
+ container: {
310
+ flex: 1,
311
+ position: 'relative',
312
+ },
313
+ scrollView: {
314
+ flex: 1,
315
+ },
316
+ content: {
317
+ flexGrow: 1,
318
+ },
319
+ centered: {
320
+ flex: 1,
321
+ justifyContent: 'center',
322
+ alignItems: 'center',
323
+ },
324
+ absoluteContainer: {
325
+ ...StyleSheet.absoluteFillObject,
326
+ zIndex: 10,
327
+ },
328
+ });
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Product Variable Provider for BillDog React Native SDK
3
+ *
4
+ * Generates variable maps from packages for use in VariableResolver.
5
+ * Enables {{product.price}}, {{product.period}}, {{selectedProduct.*}}, etc.
6
+ *
7
+ * Port of Android's ProductVariableProvider.kt with 1:1 variable key parity.
8
+ */
9
+
10
+ export interface PackageInput {
11
+ identifier: string;
12
+ packageType?: string;
13
+ productId?: string;
14
+ price?: string;
15
+ rawPrice?: number;
16
+ currencyCode?: string;
17
+ title?: string;
18
+ description?: string;
19
+ subscriptionPeriod?: { value: number; unit: string } | null;
20
+ freeTrialPeriod?: { value: number; unit: string } | null;
21
+ introductoryPrice?: { price: number; priceString: string; period?: { value: number; unit: string } } | null;
22
+ isSubscription?: boolean;
23
+ [key: string]: any;
24
+ }
25
+
26
+ export interface TrialEligibilityInput {
27
+ isEligible: boolean;
28
+ hasUsedTrial?: boolean;
29
+ [key: string]: any;
30
+ }
31
+
32
+ export class ProductVariableProvider {
33
+ private packages: PackageInput[];
34
+ private trialEligibility?: Record<string, TrialEligibilityInput>;
35
+
36
+ constructor(
37
+ packages: PackageInput[],
38
+ trialEligibility?: Record<string, TrialEligibilityInput>
39
+ ) {
40
+ this.packages = packages;
41
+ this.trialEligibility = trialEligibility;
42
+ }
43
+
44
+ /** Generate variable map for a specific product index */
45
+ getVariablesForProduct(index: number): Record<string, any> {
46
+ const pkg = this.packages[index];
47
+ if (!pkg) return {};
48
+ return this.getVariablesForPackage(pkg);
49
+ }
50
+
51
+ /** Generate variable map for a specific package (40+ keys) */
52
+ getVariablesForPackage(pkg: PackageInput): Record<string, any> {
53
+ const eligibility = this.trialEligibility?.[pkg.productId ?? pkg.identifier];
54
+ const vars: Record<string, any> = {};
55
+
56
+ // Basic
57
+ vars['product.identifier'] = pkg.identifier;
58
+ vars['product.packageType'] = pkg.packageType ?? 'custom';
59
+
60
+ const hasProduct = pkg.price !== undefined || pkg.rawPrice !== undefined;
61
+
62
+ if (hasProduct) {
63
+ vars['product.price'] = pkg.price ?? '';
64
+ vars['product.rawPrice'] = pkg.rawPrice ?? 0;
65
+ vars['product.currencyCode'] = pkg.currencyCode ?? 'USD';
66
+ vars['product.currencySymbol'] = getCurrencySymbol(pkg.currencyCode ?? 'USD');
67
+ vars['product.title'] = pkg.title ?? '';
68
+ vars['product.description'] = pkg.description ?? '';
69
+
70
+ // Period
71
+ const period = pkg.subscriptionPeriod;
72
+ if (period) {
73
+ vars['product.period'] = toDisplayString(period);
74
+ vars['product.periodShort'] = toShortString(period);
75
+ vars['product.periodDays'] = totalDays(period);
76
+ vars['product.periodUnit'] = period.unit;
77
+ vars['product.periodValue'] = period.value;
78
+ }
79
+
80
+ // Calculated prices
81
+ const raw = pkg.rawPrice ?? 0;
82
+ const days = period ? totalDays(period) : 0;
83
+ const daily = days > 0 ? raw / days : raw;
84
+ const weekly = days > 0 ? (raw / days) * 7 : raw;
85
+ const monthly = days > 0 ? raw / (days / 30) : raw;
86
+ const yearly = days > 0 ? (raw / days) * 365 : raw;
87
+ const cc = pkg.currencyCode ?? 'USD';
88
+
89
+ vars['product.dailyPrice'] = formatPrice(daily, cc);
90
+ vars['product.weeklyPrice'] = formatPrice(weekly, cc);
91
+ vars['product.monthlyPrice'] = formatPrice(monthly, cc);
92
+ vars['product.yearlyPrice'] = formatPrice(yearly, cc);
93
+ vars['product.rawDailyPrice'] = daily;
94
+ vars['product.rawWeeklyPrice'] = weekly;
95
+ vars['product.rawMonthlyPrice'] = monthly;
96
+ vars['product.rawYearlyPrice'] = yearly;
97
+
98
+ // Trial
99
+ const trial = pkg.freeTrialPeriod;
100
+ vars['product.hasFreeTrial'] = trial != null;
101
+ if (trial) {
102
+ vars['product.trialPeriod'] = toDisplayString(trial);
103
+ vars['product.trialPeriodDays'] = totalDays(trial);
104
+ vars['product.trialPeriodText'] = formatTrialPeriod(trial);
105
+ } else {
106
+ vars['product.trialPeriod'] = '';
107
+ vars['product.trialPeriodDays'] = 0;
108
+ vars['product.trialPeriodText'] = '';
109
+ }
110
+
111
+ vars['product.isTrialEligible'] = eligibility?.isEligible ?? (trial != null);
112
+
113
+ // Intro offer
114
+ const intro = pkg.introductoryPrice;
115
+ vars['product.hasIntroOffer'] = intro != null;
116
+ if (intro) {
117
+ vars['product.introPrice'] = intro.priceString;
118
+ vars['product.introPriceRaw'] = intro.price;
119
+ vars['product.introPeriod'] = intro.period ? toDisplayString(intro.period) : '';
120
+ } else {
121
+ vars['product.introPrice'] = '';
122
+ vars['product.introPriceRaw'] = 0;
123
+ vars['product.introPeriod'] = '';
124
+ }
125
+
126
+ // User eligibility
127
+ const isEligibleForIntro = eligibility?.isEligible ?? (trial != null || intro != null);
128
+ const hasUsedTrial = eligibility?.hasUsedTrial ?? false;
129
+ vars['user.isEligibleForIntro'] = isEligibleForIntro;
130
+ vars['user.hasUsedTrial'] = hasUsedTrial;
131
+ vars['user.isEligibleForPromo'] = false;
132
+
133
+ // Offer summary
134
+ const hasAnyOffer = trial != null || intro != null;
135
+ vars['offer.hasAnyOffer'] = hasAnyOffer;
136
+ vars['offer.type'] = trial ? 'free_trial' : intro ? 'intro' : 'none';
137
+
138
+ vars['product.isSubscription'] = pkg.isSubscription ?? (period != null);
139
+ } else {
140
+ // Fallback values
141
+ vars['product.price'] = '';
142
+ vars['product.rawPrice'] = 0;
143
+ vars['product.currencyCode'] = 'USD';
144
+ vars['product.currencySymbol'] = '$';
145
+ vars['product.title'] = '';
146
+ vars['product.description'] = '';
147
+ vars['product.period'] = '';
148
+ vars['product.periodShort'] = '';
149
+ vars['product.periodDays'] = 0;
150
+ vars['product.hasFreeTrial'] = false;
151
+ vars['product.isTrialEligible'] = false;
152
+ vars['product.hasIntroOffer'] = false;
153
+ vars['product.isSubscription'] = false;
154
+ }
155
+
156
+ return vars;
157
+ }
158
+
159
+ /** Generate variables for selected product with "selectedProduct." prefix */
160
+ getSelectedProductVariables(selectedIndex: number): Record<string, any> {
161
+ const productVars = this.getVariablesForProduct(selectedIndex);
162
+ const result: Record<string, any> = {};
163
+ for (const [key, value] of Object.entries(productVars)) {
164
+ result[key.replace('product.', 'selectedProduct.')] = value;
165
+ }
166
+ return result;
167
+ }
168
+
169
+ /** Generate all product variables with index prefix (products.0.*, products.1.*, etc.) */
170
+ getAllProductVariables(): Record<string, any> {
171
+ const vars: Record<string, any> = {};
172
+ this.packages.forEach((pkg, index) => {
173
+ const pkgVars = this.getVariablesForPackage(pkg);
174
+ for (const [key, value] of Object.entries(pkgVars)) {
175
+ const productKey = key.startsWith('product.') ? key.slice(8) : key;
176
+ vars[`products.${index}.${productKey}`] = value;
177
+ }
178
+ });
179
+ return vars;
180
+ }
181
+
182
+ /** Generate convenience aliases (primary, secondary, monthly, annual, etc.) */
183
+ getConvenienceVariables(): Record<string, any> {
184
+ const vars: Record<string, any> = {};
185
+
186
+ // Primary = first package
187
+ if (this.packages[0]) {
188
+ for (const [key, value] of Object.entries(this.getVariablesForPackage(this.packages[0]))) {
189
+ const k = key.startsWith('product.') ? key.slice(8) : key;
190
+ vars[`primary.${k}`] = value;
191
+ }
192
+ }
193
+
194
+ // Secondary = second package
195
+ if (this.packages[1]) {
196
+ for (const [key, value] of Object.entries(this.getVariablesForPackage(this.packages[1]))) {
197
+ const k = key.startsWith('product.') ? key.slice(8) : key;
198
+ vars[`secondary.${k}`] = value;
199
+ }
200
+ }
201
+
202
+ // Named packages by type
203
+ for (const pkg of this.packages) {
204
+ const typeKey = pkg.packageType ?? 'custom';
205
+ for (const [key, value] of Object.entries(this.getVariablesForPackage(pkg))) {
206
+ const k = key.startsWith('product.') ? key.slice(8) : key;
207
+ vars[`${typeKey}.${k}`] = value;
208
+ }
209
+ }
210
+
211
+ return vars;
212
+ }
213
+
214
+ /** Create provider from an offering */
215
+ static fromOffering(
216
+ offering: { packages: PackageInput[] },
217
+ trialEligibility?: Record<string, TrialEligibilityInput>
218
+ ): ProductVariableProvider {
219
+ return new ProductVariableProvider(offering.packages, trialEligibility);
220
+ }
221
+ }
222
+
223
+ // Helpers
224
+
225
+ function getCurrencySymbol(currencyCode: string): string {
226
+ try {
227
+ return (0).toLocaleString(undefined, {
228
+ style: 'currency', currency: currencyCode, minimumFractionDigits: 0, maximumFractionDigits: 0,
229
+ }).replace(/\d/g, '').trim() || '$';
230
+ } catch {
231
+ return '$';
232
+ }
233
+ }
234
+
235
+ function formatPrice(price: number, currencyCode: string): string {
236
+ try {
237
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency: currencyCode }).format(price);
238
+ } catch {
239
+ return `$${price.toFixed(2)}`;
240
+ }
241
+ }
242
+
243
+ function totalDays(period: { value: number; unit: string }): number {
244
+ switch (period.unit) {
245
+ case 'day': return period.value;
246
+ case 'week': return period.value * 7;
247
+ case 'month': return period.value * 30;
248
+ case 'year': return period.value * 365;
249
+ default: return period.value;
250
+ }
251
+ }
252
+
253
+ function toDisplayString(period: { value: number; unit: string }): string {
254
+ const u = period.unit;
255
+ const v = period.value;
256
+ const unitStr = v === 1
257
+ ? (u === 'day' ? 'day' : u === 'week' ? 'week' : u === 'month' ? 'month' : 'year')
258
+ : (u === 'day' ? 'days' : u === 'week' ? 'weeks' : u === 'month' ? 'months' : 'years');
259
+ return `${v} ${unitStr}`;
260
+ }
261
+
262
+ function toShortString(period: { value: number; unit: string }): string {
263
+ switch (period.unit) {
264
+ case 'day': return '/day';
265
+ case 'week': return '/wk';
266
+ case 'month': return '/mo';
267
+ case 'year': return '/yr';
268
+ default: return `/${period.unit}`;
269
+ }
270
+ }
271
+
272
+ function formatTrialPeriod(period: { value: number; unit: string }): string {
273
+ const days = totalDays(period);
274
+ if (days === 1) return '1-day free trial';
275
+ if (days === 7) return '7-day free trial';
276
+ if (days === 14) return '2-week free trial';
277
+ if (days === 30 || days === 31) return '1-month free trial';
278
+ if (days < 7) return `${days}-day free trial`;
279
+ if (days % 7 === 0) return `${days / 7}-week free trial`;
280
+ if (days % 30 === 0) return `${days / 30}-month free trial`;
281
+ return `${days}-day free trial`;
282
+ }