@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,516 @@
1
+ /**
2
+ * Client-side variable resolver for React Native
3
+ * Resolves ${variable} and {{variable}} placeholders at runtime
4
+ * Supports localization integration for localized variables
5
+ */
6
+
7
+ import { LocalizationResolver } from './LocalizationResolver';
8
+ import { PaywallConfiguration, PaywallComponent } from './types';
9
+ import { processXFlowTags } from './xFlowTags';
10
+ import { applyFilterChain } from './filters';
11
+ import { ProductVariableProvider, PackageInput, TrialEligibilityInput } from './ProductVariableProvider';
12
+ import { userSeedBucket } from './murmur';
13
+
14
+ /**
15
+ * Get device locale with safe fallback for React Native
16
+ */
17
+ function deviceLocale(): string {
18
+ try {
19
+ return Intl.DateTimeFormat().resolvedOptions().locale;
20
+ } catch {
21
+ return 'en';
22
+ }
23
+ }
24
+
25
+ /** Properties eligible for localization (matches Android/iOS LOCALIZABLE_PROPERTIES) */
26
+ const LOCALIZABLE_PROPERTIES = new Set([
27
+ 'text', 'title', 'subtitle', 'label', 'placeholder',
28
+ 'buttonText', 'ctaText', 'description', 'content',
29
+ 'headerText', 'footerText', 'disclaimer',
30
+ ]);
31
+
32
+ export interface DeviceInfo {
33
+ osName?: string;
34
+ osVersion?: string;
35
+ platform?: string;
36
+ deviceModel?: string;
37
+ screenWidth?: number;
38
+ screenHeight?: number;
39
+ locale?: string;
40
+ timezone?: string;
41
+ interfaceStyle?: 'light' | 'dark' | 'unspecified';
42
+ batteryLevel?: number;
43
+ isLowPowerMode?: boolean;
44
+ networkType?: string;
45
+ carrierName?: string;
46
+ }
47
+
48
+ export class VariableResolver {
49
+ private allVariables: Record<string, any>;
50
+ private localizationResolver?: LocalizationResolver;
51
+ private variableLocalizations?: Record<string, Record<string, string>>;
52
+ private currentLocale?: string;
53
+
54
+ constructor(
55
+ configVariables?: Record<string, string> | null,
56
+ runtimeVariables: Record<string, any> = {},
57
+ localizationResolver?: LocalizationResolver,
58
+ variableLocalizations?: Record<string, Record<string, string>>,
59
+ currentLocale?: string
60
+ ) {
61
+ this.allVariables = {
62
+ ...configVariables,
63
+ ...runtimeVariables,
64
+ };
65
+ this.localizationResolver = localizationResolver;
66
+ this.variableLocalizations = variableLocalizations;
67
+ this.currentLocale = currentLocale;
68
+ }
69
+
70
+ /**
71
+ * Resolve a single string with variables
72
+ */
73
+ resolve(text?: string | null): string {
74
+ if (!text) return text ?? '';
75
+
76
+ let result = text;
77
+
78
+ // Local copy — xFlow assigns must not leak across resolve() calls
79
+ let resolveVars = { ...this.allVariables };
80
+
81
+ // Process xFlow {% %} tags before {{ }} interpolation
82
+ if (result.includes('{%')) {
83
+ const xFlowResult = processXFlowTags(result, resolveVars);
84
+ result = xFlowResult.text;
85
+ // Merge assigned variables into local context only
86
+ resolveVars = { ...resolveVars, ...xFlowResult.variables };
87
+ }
88
+
89
+ // Resolve ${variable} syntax
90
+ result = result.replace(/\$\{([\w.]+)\}/g, (_, varName) => {
91
+ return this.resolveVariablePath(varName, resolveVars);
92
+ });
93
+
94
+ // Resolve {{ variable }} and {{ variable | filter: arg }} syntax
95
+ result = result.replace(
96
+ /\{\{\s*([\w.]+)(?:\s*\|\s*(.+?))?\s*\}\}/g,
97
+ (match, varName, filterChain) => {
98
+ const name = varName.trim();
99
+ if (!filterChain) {
100
+ return this.resolveVariablePath(name, resolveVars);
101
+ }
102
+ // Filter chains operate on the raw value, bypassing localization.
103
+ const raw = this.resolveRawValue(name, resolveVars);
104
+ if (raw === undefined) return match;
105
+ let value: any = raw;
106
+ try {
107
+ value = applyFilterChain(raw, filterChain.trim());
108
+ } catch (error) {
109
+ console.warn('[VariableResolver] Filter chain failed:', error);
110
+ }
111
+ return value !== undefined && value !== null ? String(value) : match;
112
+ },
113
+ );
114
+
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * Resolve a variable path to its raw (un-stringified) value.
120
+ * Flat-key-first (grammar §4): a literal key matching the whole path wins.
121
+ * Returns undefined when unresolved.
122
+ */
123
+ private resolveRawValue(path: string, from: Record<string, any>): any {
124
+ if (path in from) return from[path];
125
+ const parts = path.split('.');
126
+ let current: any = from[parts[0]];
127
+ for (let i = 1; i < parts.length; i++) {
128
+ if (current && typeof current === 'object') {
129
+ current = current[parts[i]];
130
+ } else {
131
+ return undefined;
132
+ }
133
+ }
134
+ return current;
135
+ }
136
+
137
+ /**
138
+ * Resolve nested variable paths like "offering.monthly"
139
+ */
140
+ private resolveVariablePath(path: string, from?: Record<string, any>): string {
141
+ const vars = from ?? this.allVariables;
142
+
143
+ // Priority 1: Check variableLocalizations[path][locale]
144
+ if (this.variableLocalizations && this.currentLocale) {
145
+ const localeMap = this.variableLocalizations[path];
146
+ if (localeMap) {
147
+ const localizedValue = localeMap[this.currentLocale]
148
+ ?? localeMap[this.currentLocale.split('-')[0]]
149
+ ?? localeMap[this.currentLocale.split('_')[0]];
150
+ if (localizedValue !== undefined) {
151
+ return localizedValue;
152
+ }
153
+ }
154
+ }
155
+
156
+ // Priority 2: Direct lookup
157
+ if (path in vars) {
158
+ return String(vars[path]);
159
+ }
160
+
161
+ // Priority 3: Try nested path resolution
162
+ const parts = path.split('.');
163
+ let current: any = undefined;
164
+
165
+ for (const part of parts) {
166
+ if (current === undefined) {
167
+ current = vars[part];
168
+ } else if (typeof current === 'object' && current !== null) {
169
+ current = current[part];
170
+ } else {
171
+ current = undefined;
172
+ break;
173
+ }
174
+ }
175
+
176
+ if (current !== undefined) {
177
+ return String(current);
178
+ }
179
+
180
+ // Return empty string for unresolved variables to match the unified parity
181
+ // contract: iOS VariableResolver.swift:177 and Android
182
+ // VariableResolver.kt:147 both substitute "". A `${path}` placeholder is
183
+ // editor-mode behavior and must not leak into runtime — an unresolved
184
+ // variable would otherwise render as literal `${user.name}` on-device.
185
+ //
186
+ // This fix existed in the legacy packages/react-native resolver and was
187
+ // lost when the thin bridge replaced it (ba6a68d86); the conformance
188
+ // corpus caught the regression. Warn rather than log so unresolved
189
+ // variables stay visible in production, matching both native SDKs.
190
+ console.warn(`[VariableResolver] Variable not resolved: ${path} — substituting empty string`);
191
+ return '';
192
+ }
193
+
194
+ /**
195
+ * Resolve all text properties in a component
196
+ * Applies localization for LOCALIZABLE_PROPERTIES (matching Android/iOS parity)
197
+ */
198
+ resolveComponent(component: PaywallComponent): PaywallComponent {
199
+ if (!component) return component;
200
+
201
+ const resolvedComponent = { ...component };
202
+
203
+ // Resolve properties
204
+ if (component.properties) {
205
+ const resolvedProperties: Record<string, any> = {};
206
+ for (const [key, value] of Object.entries(component.properties)) {
207
+ if (typeof value === 'string') {
208
+ const variableResolved = this.resolve(value);
209
+ // Apply localization for localizable properties (matching Android/iOS)
210
+ if (this.localizationResolver
211
+ && LOCALIZABLE_PROPERTIES.has(key)
212
+ && !this.localizationResolver.isBaseLocale()) {
213
+ resolvedProperties[key] = this.localizationResolver.resolve(
214
+ `${component.id}.${key}`,
215
+ variableResolved
216
+ );
217
+ } else {
218
+ resolvedProperties[key] = variableResolved;
219
+ }
220
+ } else {
221
+ resolvedProperties[key] = value;
222
+ }
223
+ }
224
+ resolvedComponent.properties = resolvedProperties;
225
+ }
226
+
227
+ // Recursively resolve children
228
+ if (component.children && Array.isArray(component.children)) {
229
+ resolvedComponent.children = component.children.map((child: PaywallComponent) =>
230
+ this.resolveComponent(child)
231
+ );
232
+ }
233
+
234
+ return resolvedComponent;
235
+ }
236
+
237
+ /**
238
+ * Resolve all components in a configuration
239
+ */
240
+ resolveConfiguration(config: PaywallConfiguration): PaywallConfiguration {
241
+ if (!config) return config;
242
+
243
+ const resolvedConfig = { ...config };
244
+
245
+ if (config.components && Array.isArray(config.components)) {
246
+ resolvedConfig.components = config.components.map((comp: PaywallComponent) =>
247
+ this.resolveComponent(comp)
248
+ );
249
+ }
250
+
251
+ return resolvedConfig;
252
+ }
253
+
254
+ // ─── Device Variable Builder (single source of truth) ──────────────────────
255
+
256
+ /**
257
+ * Build device variable map from DeviceInfo — single source of truth.
258
+ * Used by withDeviceInfo(), withFullContext(), withProducts().
259
+ * Maps RN DeviceInfo field names to canonical SDK key names.
260
+ */
261
+ static buildDeviceVariables(deviceInfo: DeviceInfo): Record<string, any> {
262
+ const vars: Record<string, any> = {};
263
+
264
+ // Flat keys for direct access (canonical names matching Android/iOS)
265
+ if (deviceInfo.osName) vars['device.osName'] = deviceInfo.osName;
266
+ if (deviceInfo.osVersion) vars['device.osVersion'] = deviceInfo.osVersion;
267
+ if (deviceInfo.platform) vars['device.platform'] = deviceInfo.platform;
268
+ if (deviceInfo.deviceModel) vars['device.deviceModel'] = deviceInfo.deviceModel;
269
+ if (deviceInfo.screenWidth !== undefined) vars['device.screenWidth'] = deviceInfo.screenWidth;
270
+ if (deviceInfo.screenHeight !== undefined) vars['device.screenHeight'] = deviceInfo.screenHeight;
271
+ if (deviceInfo.locale) vars['device.locale'] = deviceInfo.locale;
272
+ if (deviceInfo.timezone) vars['device.timezone'] = deviceInfo.timezone;
273
+ if (deviceInfo.interfaceStyle) vars['device.interfaceStyle'] = deviceInfo.interfaceStyle;
274
+ if (deviceInfo.batteryLevel !== undefined) vars['device.batteryLevel'] = deviceInfo.batteryLevel;
275
+ // Map RN field name to canonical SDK key name
276
+ if (deviceInfo.isLowPowerMode !== undefined) vars['device.isLowPowerModeEnabled'] = deviceInfo.isLowPowerMode;
277
+ if (deviceInfo.networkType) vars['device.radioType'] = deviceInfo.networkType;
278
+ if (deviceInfo.carrierName) vars['device.carrierName'] = deviceInfo.carrierName;
279
+
280
+ // Nested object for dot-path resolution (includes screenWidth/Height)
281
+ vars.device = {
282
+ osName: deviceInfo.osName,
283
+ osVersion: deviceInfo.osVersion,
284
+ platform: deviceInfo.platform,
285
+ deviceModel: deviceInfo.deviceModel,
286
+ locale: deviceInfo.locale,
287
+ timezone: deviceInfo.timezone,
288
+ interfaceStyle: deviceInfo.interfaceStyle,
289
+ screenWidth: deviceInfo.screenWidth,
290
+ screenHeight: deviceInfo.screenHeight,
291
+ };
292
+
293
+ return vars;
294
+ }
295
+
296
+ // ─── Factory Methods ───────────────────────────────────────────────────────
297
+
298
+ /**
299
+ * Create resolver with offering variables injected
300
+ */
301
+ static withOfferings(
302
+ config: PaywallConfiguration,
303
+ offerings: Record<string, string>
304
+ ): VariableResolver {
305
+ return new VariableResolver(
306
+ config.variables,
307
+ offerings,
308
+ LocalizationResolver.fromConfiguration(config),
309
+ config.variableLocalizations,
310
+ deviceLocale()
311
+ );
312
+ }
313
+
314
+ /**
315
+ * Create resolver with device info variables
316
+ * Maps device info to namespaced variables (device.*)
317
+ */
318
+ static withDeviceInfo(
319
+ config: PaywallConfiguration,
320
+ deviceInfo: DeviceInfo
321
+ ): VariableResolver {
322
+ return new VariableResolver(
323
+ config.variables,
324
+ VariableResolver.buildDeviceVariables(deviceInfo),
325
+ LocalizationResolver.fromConfiguration(config),
326
+ config.variableLocalizations,
327
+ deviceInfo.locale ?? deviceLocale()
328
+ );
329
+ }
330
+
331
+ /**
332
+ * Create resolver with localization support
333
+ */
334
+ static withLocalization(
335
+ config: PaywallConfiguration,
336
+ locale?: string
337
+ ): VariableResolver {
338
+ const locResolver = locale
339
+ ? LocalizationResolver.withLocale(config, locale)
340
+ : LocalizationResolver.withDeviceLocale(config);
341
+
342
+ return new VariableResolver(
343
+ config.variables,
344
+ {},
345
+ locResolver,
346
+ config.variableLocalizations,
347
+ locale ?? deviceLocale()
348
+ );
349
+ }
350
+
351
+ /**
352
+ * Create resolver with full context (offerings, device info, localization)
353
+ */
354
+ static withFullContext(
355
+ config: PaywallConfiguration,
356
+ offerings: Record<string, string>,
357
+ deviceInfo: DeviceInfo
358
+ ): VariableResolver {
359
+ const runtimeVars = {
360
+ ...VariableResolver.buildDeviceVariables(deviceInfo),
361
+ ...offerings,
362
+ };
363
+
364
+ return new VariableResolver(
365
+ config.variables,
366
+ runtimeVars,
367
+ LocalizationResolver.withDeviceLocale(config),
368
+ config.variableLocalizations,
369
+ deviceInfo.locale ?? deviceLocale()
370
+ );
371
+ }
372
+
373
+ /**
374
+ * Create resolver with user eligibility variables
375
+ * Enables {{user.isEligibleForIntro}}, {{user.hasUsedTrial}}, etc.
376
+ */
377
+ static withEligibility(
378
+ config: PaywallConfiguration,
379
+ isEligibleForIntro: boolean = true,
380
+ hasUsedTrial: boolean = false,
381
+ isEligibleForPromo: boolean = false
382
+ ): VariableResolver {
383
+ const eligibilityVars: Record<string, any> = {
384
+ 'user.isEligibleForIntro': isEligibleForIntro,
385
+ 'user.hasUsedTrial': hasUsedTrial,
386
+ 'user.isEligibleForPromo': isEligibleForPromo,
387
+ user: {
388
+ isEligibleForIntro,
389
+ hasUsedTrial,
390
+ isEligibleForPromo,
391
+ },
392
+ };
393
+
394
+ return new VariableResolver(
395
+ config.variables,
396
+ eligibilityVars,
397
+ LocalizationResolver.fromConfiguration(config),
398
+ config.variableLocalizations,
399
+ deviceLocale()
400
+ );
401
+ }
402
+
403
+ /**
404
+ * Create resolver with product/package variables
405
+ * Enables {{product.price}}, {{selectedProduct.*}}, {{products.N.*}},
406
+ * {{primary.*}}, {{monthly.*}}, etc. (40+ variable keys per package)
407
+ *
408
+ * Delegates to ProductVariableProvider for full parity with Android.
409
+ */
410
+ static withProducts(
411
+ config: PaywallConfiguration,
412
+ packages: PackageInput[],
413
+ selectedIndex: number = 0,
414
+ trialEligibility?: Record<string, TrialEligibilityInput>,
415
+ deviceInfo?: DeviceInfo
416
+ ): VariableResolver {
417
+ const provider = new ProductVariableProvider(packages, trialEligibility);
418
+ const productVars: Record<string, any> = {};
419
+
420
+ // Selected product → product.* prefix
421
+ Object.assign(productVars, provider.getVariablesForProduct(selectedIndex));
422
+
423
+ // selectedProduct.* mirror
424
+ Object.assign(productVars, provider.getSelectedProductVariables(selectedIndex));
425
+
426
+ // products.N.* indexed access
427
+ Object.assign(productVars, provider.getAllProductVariables());
428
+ productVars['packages.count'] = packages.length;
429
+
430
+ // Convenience aliases (primary.*, secondary.*, monthly.*, annual.*, etc.)
431
+ Object.assign(productVars, provider.getConvenienceVariables());
432
+
433
+ // Add device info if provided — use canonical buildDeviceVariables
434
+ if (deviceInfo) {
435
+ Object.assign(productVars, VariableResolver.buildDeviceVariables(deviceInfo));
436
+ }
437
+
438
+ return new VariableResolver(
439
+ config.variables,
440
+ productVars,
441
+ LocalizationResolver.fromConfiguration(config),
442
+ config.variableLocalizations,
443
+ deviceInfo?.locale ?? deviceLocale()
444
+ );
445
+ }
446
+
447
+ /**
448
+ * Create resolver with an Offering's packages
449
+ * Convenience wrapper around withProducts
450
+ */
451
+ static withOffering(
452
+ config: PaywallConfiguration,
453
+ offering: { packages: PackageInput[] },
454
+ selectedIndex: number = 0,
455
+ trialEligibility?: Record<string, TrialEligibilityInput>,
456
+ deviceInfo?: DeviceInfo
457
+ ): VariableResolver {
458
+ return VariableResolver.withProducts(config, offering.packages, selectedIndex, trialEligibility, deviceInfo);
459
+ }
460
+
461
+ /**
462
+ * Build the FULL render-time variable bag (device + product/offer/user) — the
463
+ * React-Native analog of iOS/Android `VariableResolver.withOffering(...).variables`.
464
+ * Fed to `processFullComponent` so Dynamic Values conditions on
465
+ * `products.*` / `selectedProduct.*` / `offer.*` / `user.*` actually resolve on
466
+ * RN (previously the render bag was device + `config.variables` only, so every
467
+ * product/user DV condition silently rendered `default`).
468
+ *
469
+ * `selectedIndex` is 0 for now — parity with the native Phase-2 render pass,
470
+ * which is not yet selection-reactive (Phase 2b). `appUserId`/`customerInfo` are
471
+ * passed in by the caller (`BillDog.getAppUserId()` + `customerInfoEmitter.latest`,
472
+ * both sync) to avoid a VariableResolver→BillDog import cycle.
473
+ */
474
+ static buildRenderVariables(
475
+ config: PaywallConfiguration,
476
+ deviceInfo: DeviceInfo,
477
+ opts?: {
478
+ selectedIndex?: number;
479
+ appUserId?: string | null;
480
+ customerInfo?: {
481
+ entitlements?: Array<{ identifier: string; isActive: boolean }>;
482
+ activeSubscriptions?: string[];
483
+ } | null;
484
+ }
485
+ ): Record<string, any> {
486
+ const selectedIndex = opts?.selectedIndex ?? 0;
487
+ const bag: Record<string, any> = { ...VariableResolver.buildDeviceVariables(deviceInfo) };
488
+
489
+ const packages = (config as any).packages as PackageInput[] | undefined;
490
+ if (Array.isArray(packages) && packages.length > 0) {
491
+ const provider = new ProductVariableProvider(packages, (config as any).trialEligibility);
492
+ Object.assign(bag, provider.getVariablesForProduct(selectedIndex)); // product.* (+ user.isEligibleForIntro/hasUsedTrial)
493
+ Object.assign(bag, provider.getSelectedProductVariables(selectedIndex)); // selectedProduct.*
494
+ Object.assign(bag, provider.getAllProductVariables()); // products.N.*
495
+ Object.assign(bag, provider.getConvenienceVariables()); // primary.*/secondary.*/…
496
+ bag['packages.count'] = packages.length;
497
+ bag['products.selectedIndex'] = selectedIndex;
498
+ }
499
+
500
+ // user.seed — deterministic A/B bucket (canonical murmur; parity with native).
501
+ if (opts?.appUserId) bag['user.seed'] = userSeedBucket(opts.appUserId);
502
+
503
+ // user.subscription — active entitlement tier, from the SYNC-cached CustomerInfo
504
+ // (may be null on cold start; best-effort, same async limitation as iOS Phase 2).
505
+ // Sorted for cross-platform determinism with iOS/Android.
506
+ const customer = opts?.customerInfo;
507
+ if (customer) {
508
+ const active = (customer.entitlements ?? []).filter((e) => e.isActive).map((e) => e.identifier).sort();
509
+ bag['user.subscription'] = active[0] ?? '';
510
+ bag['user.hasActiveSubscription'] = (customer.activeSubscriptions?.length ?? 0) > 0 || active.length > 0;
511
+ bag['user.activeEntitlements'] = active.join(',');
512
+ }
513
+
514
+ return bag;
515
+ }
516
+ }