@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.
- package/BillDogReactNative.podspec +30 -0
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/android/build.gradle +99 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogABTestModule.kt +317 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogAnalyticsModule.kt +389 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogModule.kt +817 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogPaywallViewManager.kt +383 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogSurveyModule.kt +427 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogVirtualCurrencyModule.kt +407 -0
- package/android/src/main/kotlin/io/billdog/virtualcurrency/BillDogVirtualCurrencyPackage.kt +44 -0
- package/dist/AdvancedPaywallCache.d.ts +70 -0
- package/dist/AdvancedPaywallCache.js +344 -0
- package/dist/BillDog.d.ts +255 -0
- package/dist/BillDog.js +918 -0
- package/dist/BillDogConfiguration.d.ts +145 -0
- package/dist/BillDogConfiguration.js +229 -0
- package/dist/ComponentRenderer.d.ts +17 -0
- package/dist/ComponentRenderer.js +886 -0
- package/dist/LayoutEngine.d.ts +101 -0
- package/dist/LayoutEngine.js +394 -0
- package/dist/LocalizationResolver.d.ts +99 -0
- package/dist/LocalizationResolver.js +266 -0
- package/dist/NativePaywallView.d.ts +36 -0
- package/dist/NativePaywallView.js +59 -0
- package/dist/PaywallCache.d.ts +74 -0
- package/dist/PaywallCache.js +293 -0
- package/dist/PaywallStateManager.d.ts +62 -0
- package/dist/PaywallStateManager.js +147 -0
- package/dist/PaywallView.d.ts +42 -0
- package/dist/PaywallView.js +275 -0
- package/dist/ProductVariableProvider.d.ts +60 -0
- package/dist/ProductVariableProvider.js +239 -0
- package/dist/VariableResolver.d.ts +120 -0
- package/dist/VariableResolver.js +379 -0
- package/dist/WebViewPaywall.d.ts +75 -0
- package/dist/WebViewPaywall.js +360 -0
- package/dist/animations/presets.d.ts +49 -0
- package/dist/animations/presets.js +367 -0
- package/dist/components/AccoladeCpsComponent.d.ts +13 -0
- package/dist/components/AccoladeCpsComponent.js +248 -0
- package/dist/components/AccordionComponent.d.ts +10 -0
- package/dist/components/AccordionComponent.js +187 -0
- package/dist/components/AnimatedWrapper.d.ts +35 -0
- package/dist/components/AnimatedWrapper.js +277 -0
- package/dist/components/CarouselComponent.d.ts +23 -0
- package/dist/components/CarouselComponent.js +484 -0
- package/dist/components/ChartCurveComponent.d.ts +13 -0
- package/dist/components/ChartCurveComponent.js +333 -0
- package/dist/components/CheckboxComponent.d.ts +9 -0
- package/dist/components/CheckboxComponent.js +172 -0
- package/dist/components/ConfettiComponent.d.ts +9 -0
- package/dist/components/ConfettiComponent.js +172 -0
- package/dist/components/CountdownComponent.d.ts +16 -0
- package/dist/components/CountdownComponent.js +414 -0
- package/dist/components/DelayedXComponent.d.ts +12 -0
- package/dist/components/DelayedXComponent.js +240 -0
- package/dist/components/DrawerModalComponent.d.ts +20 -0
- package/dist/components/DrawerModalComponent.js +286 -0
- package/dist/components/GradientView.d.ts +38 -0
- package/dist/components/GradientView.js +126 -0
- package/dist/components/HapticFeedbackComponent.d.ts +16 -0
- package/dist/components/HapticFeedbackComponent.js +64 -0
- package/dist/components/HorizontalProductsComponent.d.ts +17 -0
- package/dist/components/HorizontalProductsComponent.js +284 -0
- package/dist/components/MultiChoiceGridComponent.d.ts +13 -0
- package/dist/components/MultiChoiceGridComponent.js +234 -0
- package/dist/components/NPSScaleComponent.d.ts +16 -0
- package/dist/components/NPSScaleComponent.js +260 -0
- package/dist/components/NavigationComponent.d.ts +26 -0
- package/dist/components/NavigationComponent.js +302 -0
- package/dist/components/OAuthButtonComponent.d.ts +20 -0
- package/dist/components/OAuthButtonComponent.js +237 -0
- package/dist/components/ProgressLoaderComponent.d.ts +13 -0
- package/dist/components/ProgressLoaderComponent.js +308 -0
- package/dist/components/ProgressStepperComponent.d.ts +18 -0
- package/dist/components/ProgressStepperComponent.js +228 -0
- package/dist/components/SegmentedPickerComponent.d.ts +10 -0
- package/dist/components/SegmentedPickerComponent.js +221 -0
- package/dist/components/ShareButtonsComponent.d.ts +21 -0
- package/dist/components/ShareButtonsComponent.js +265 -0
- package/dist/components/ShareCodeComponent.d.ts +16 -0
- package/dist/components/ShareCodeComponent.js +274 -0
- package/dist/components/SingleChoiceGridComponent.d.ts +13 -0
- package/dist/components/SingleChoiceGridComponent.js +210 -0
- package/dist/components/SliderComponent.d.ts +9 -0
- package/dist/components/SliderComponent.js +263 -0
- package/dist/components/StarRatingInputComponent.d.ts +20 -0
- package/dist/components/StarRatingInputComponent.js +198 -0
- package/dist/components/StickerCompositeComponent.d.ts +13 -0
- package/dist/components/StickerCompositeComponent.js +136 -0
- package/dist/components/TabsComponent.d.ts +19 -0
- package/dist/components/TabsComponent.js +431 -0
- package/dist/components/TextInputComponent.d.ts +18 -0
- package/dist/components/TextInputComponent.js +227 -0
- package/dist/components/TimelineComponent.d.ts +19 -0
- package/dist/components/TimelineComponent.js +104 -0
- package/dist/components/ToggleComponent.d.ts +14 -0
- package/dist/components/ToggleComponent.js +132 -0
- package/dist/components/VerticalProductsComponent.d.ts +17 -0
- package/dist/components/VerticalProductsComponent.js +311 -0
- package/dist/dynamicValues.d.ts +36 -0
- package/dist/dynamicValues.js +142 -0
- package/dist/filters/dateFilters.d.ts +50 -0
- package/dist/filters/dateFilters.js +390 -0
- package/dist/filters/index.d.ts +18 -0
- package/dist/filters/index.js +253 -0
- package/dist/hooks/useOfferings.d.ts +76 -0
- package/dist/hooks/useOfferings.js +99 -0
- package/dist/hooks/usePaywall.d.ts +27 -0
- package/dist/hooks/usePaywall.js +114 -0
- package/dist/hooks/usePlacement.d.ts +34 -0
- package/dist/hooks/usePlacement.js +89 -0
- package/dist/hooks/useSubscription.d.ts +40 -0
- package/dist/hooks/useSubscription.js +97 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +198 -0
- package/dist/models/BillDogError.d.ts +40 -0
- package/dist/models/BillDogError.js +11 -0
- package/dist/models/CacheFetchPolicy.d.ts +8 -0
- package/dist/models/CacheFetchPolicy.js +4 -0
- package/dist/models/CustomProduct.d.ts +32 -0
- package/dist/models/CustomProduct.js +15 -0
- package/dist/models/CustomerInfo.d.ts +17 -0
- package/dist/models/CustomerInfo.js +11 -0
- package/dist/models/Delegate.d.ts +20 -0
- package/dist/models/Delegate.js +11 -0
- package/dist/models/EntitlementInfo.d.ts +19 -0
- package/dist/models/EntitlementInfo.js +2 -0
- package/dist/models/Placement.d.ts +54 -0
- package/dist/models/Placement.js +15 -0
- package/dist/models/PurchaseParams.d.ts +39 -0
- package/dist/models/PurchaseParams.js +60 -0
- package/dist/models/PurchaseResult.d.ts +30 -0
- package/dist/models/PurchaseResult.js +15 -0
- package/dist/models/RestoreResult.d.ts +30 -0
- package/dist/models/RestoreResult.js +22 -0
- package/dist/models/RestoredItem.d.ts +26 -0
- package/dist/models/RestoredItem.js +13 -0
- package/dist/modules/ABTest.d.ts +65 -0
- package/dist/modules/ABTest.js +113 -0
- package/dist/modules/Analytics.d.ts +91 -0
- package/dist/modules/Analytics.js +182 -0
- package/dist/modules/Survey.d.ts +65 -0
- package/dist/modules/Survey.js +82 -0
- package/dist/modules/VirtualCurrency.d.ts +63 -0
- package/dist/modules/VirtualCurrency.js +147 -0
- package/dist/murmur.d.ts +23 -0
- package/dist/murmur.js +104 -0
- package/dist/notifications/LiveActivitiesManager.d.ts +47 -0
- package/dist/notifications/LiveActivitiesManager.js +106 -0
- package/dist/notifications/NotificationScheduler.d.ts +55 -0
- package/dist/notifications/NotificationScheduler.js +199 -0
- package/dist/notifications/PurchaseNotificationHandler.d.ts +24 -0
- package/dist/notifications/PurchaseNotificationHandler.js +55 -0
- package/dist/notifications/useDeviceToken.d.ts +36 -0
- package/dist/notifications/useDeviceToken.js +97 -0
- package/dist/notifications/useLiveActivity.d.ts +39 -0
- package/dist/notifications/useLiveActivity.js +104 -0
- package/dist/review/ReviewAnalytics.d.ts +34 -0
- package/dist/review/ReviewAnalytics.js +73 -0
- package/dist/review/ReviewManager.d.ts +83 -0
- package/dist/review/ReviewManager.js +339 -0
- package/dist/review/ReviewTimingManager.d.ts +82 -0
- package/dist/review/ReviewTimingManager.js +224 -0
- package/dist/survey-core/types.d.ts +215 -0
- package/dist/survey-core/types.js +8 -0
- package/dist/types.d.ts +438 -0
- package/dist/types.js +158 -0
- package/dist/utils/ApiClient.d.ts +65 -0
- package/dist/utils/ApiClient.js +132 -0
- package/dist/utils/CSSConverter.d.ts +49 -0
- package/dist/utils/CSSConverter.js +470 -0
- package/dist/utils/animations.d.ts +41 -0
- package/dist/utils/animations.js +194 -0
- package/dist/utils/calendarIntegration.d.ts +36 -0
- package/dist/utils/calendarIntegration.js +89 -0
- package/dist/utils/configurationResolver.d.ts +23 -0
- package/dist/utils/configurationResolver.js +78 -0
- package/dist/utils/deviceDetection.d.ts +38 -0
- package/dist/utils/deviceDetection.js +59 -0
- package/dist/utils/hapticFeedback.d.ts +17 -0
- package/dist/utils/hapticFeedback.js +53 -0
- package/dist/utils/iconMapper.d.ts +26 -0
- package/dist/utils/iconMapper.js +113 -0
- package/dist/utils/notificationPermissions.d.ts +13 -0
- package/dist/utils/notificationPermissions.js +51 -0
- package/dist/utils/priceFormatting.d.ts +19 -0
- package/dist/utils/priceFormatting.js +99 -0
- package/dist/utils/shareSheet.d.ts +17 -0
- package/dist/utils/shareSheet.js +42 -0
- package/dist/utils/subscriptionUpgrade.d.ts +84 -0
- package/dist/utils/subscriptionUpgrade.js +100 -0
- package/dist/valueUtils.d.ts +48 -0
- package/dist/valueUtils.js +129 -0
- package/dist/xFlowTags.d.ts +18 -0
- package/dist/xFlowTags.js +385 -0
- package/ios/BillDogABTest.m +50 -0
- package/ios/BillDogABTest.swift +188 -0
- package/ios/BillDogAnalytics.m +105 -0
- package/ios/BillDogAnalytics.swift +324 -0
- package/ios/BillDogBridge.m +222 -0
- package/ios/BillDogBridge.swift +715 -0
- package/ios/BillDogLiveActivities.m +41 -0
- package/ios/BillDogLiveActivities.swift +104 -0
- package/ios/BillDogPaywallViewManager.m +39 -0
- package/ios/BillDogPaywallViewManager.swift +361 -0
- package/ios/BillDogSurvey.m +34 -0
- package/ios/BillDogSurvey.swift +257 -0
- package/ios/BillDogVirtualCurrencyBridge.m +63 -0
- package/ios/BillDogVirtualCurrencyBridge.swift +511 -0
- package/junit.xml +25 -0
- package/package.json +61 -0
- package/react-native.config.js +13 -0
- package/src/AdvancedPaywallCache.ts +433 -0
- package/src/BillDog.ts +983 -0
- package/src/BillDogConfiguration.ts +318 -0
- package/src/ComponentRenderer.tsx +1325 -0
- package/src/LayoutEngine.ts +459 -0
- package/src/LocalizationResolver.ts +311 -0
- package/src/NativePaywallView.tsx +132 -0
- package/src/PaywallCache.ts +358 -0
- package/src/PaywallStateManager.ts +154 -0
- package/src/PaywallView.tsx +328 -0
- package/src/ProductVariableProvider.ts +282 -0
- package/src/VariableResolver.ts +516 -0
- package/src/WebViewPaywall.tsx +454 -0
- package/src/animations/presets.ts +421 -0
- package/src/components/AccoladeCpsComponent.tsx +259 -0
- package/src/components/AccordionComponent.tsx +213 -0
- package/src/components/AnimatedWrapper.tsx +316 -0
- package/src/components/CarouselComponent.tsx +615 -0
- package/src/components/ChartCurveComponent.tsx +452 -0
- package/src/components/CheckboxComponent.tsx +185 -0
- package/src/components/ConfettiComponent.tsx +178 -0
- package/src/components/CountdownComponent.tsx +454 -0
- package/src/components/DelayedXComponent.tsx +266 -0
- package/src/components/DrawerModalComponent.tsx +324 -0
- package/src/components/GradientView.tsx +129 -0
- package/src/components/HapticFeedbackComponent.tsx +39 -0
- package/src/components/HorizontalProductsComponent.tsx +329 -0
- package/src/components/MultiChoiceGridComponent.tsx +265 -0
- package/src/components/NPSScaleComponent.tsx +294 -0
- package/src/components/NavigationComponent.tsx +329 -0
- package/src/components/OAuthButtonComponent.tsx +266 -0
- package/src/components/ProgressLoaderComponent.tsx +366 -0
- package/src/components/ProgressStepperComponent.tsx +250 -0
- package/src/components/SegmentedPickerComponent.tsx +245 -0
- package/src/components/ShareButtonsComponent.tsx +299 -0
- package/src/components/ShareCodeComponent.tsx +303 -0
- package/src/components/SingleChoiceGridComponent.tsx +229 -0
- package/src/components/SliderComponent.tsx +280 -0
- package/src/components/StarRatingInputComponent.tsx +223 -0
- package/src/components/StickerCompositeComponent.tsx +168 -0
- package/src/components/TabsComponent.tsx +496 -0
- package/src/components/TextInputComponent.tsx +258 -0
- package/src/components/TimelineComponent.tsx +129 -0
- package/src/components/ToggleComponent.tsx +142 -0
- package/src/components/VerticalProductsComponent.tsx +357 -0
- package/src/dynamicValues.ts +180 -0
- package/src/filters/dateFilters.ts +411 -0
- package/src/filters/index.ts +281 -0
- package/src/hooks/useOfferings.ts +192 -0
- package/src/hooks/usePaywall.ts +154 -0
- package/src/hooks/usePlacement.ts +143 -0
- package/src/hooks/useSubscription.ts +154 -0
- package/src/index.ts +245 -0
- package/src/models/BillDogError.ts +24 -0
- package/src/models/CacheFetchPolicy.ts +13 -0
- package/src/models/CustomProduct.ts +33 -0
- package/src/models/CustomerInfo.ts +26 -0
- package/src/models/Delegate.ts +22 -0
- package/src/models/EntitlementInfo.ts +27 -0
- package/src/models/Placement.ts +50 -0
- package/src/models/PurchaseParams.ts +81 -0
- package/src/models/PurchaseResult.ts +32 -0
- package/src/models/RestoreResult.ts +27 -0
- package/src/models/RestoredItem.ts +25 -0
- package/src/modules/ABTest.ts +131 -0
- package/src/modules/Analytics.ts +227 -0
- package/src/modules/Survey.ts +126 -0
- package/src/modules/VirtualCurrency.ts +223 -0
- package/src/murmur.ts +102 -0
- package/src/notifications/LiveActivitiesManager.ts +140 -0
- package/src/notifications/NotificationScheduler.ts +292 -0
- package/src/notifications/PurchaseNotificationHandler.ts +67 -0
- package/src/notifications/useDeviceToken.ts +116 -0
- package/src/notifications/useLiveActivity.ts +125 -0
- package/src/review/ReviewAnalytics.ts +89 -0
- package/src/review/ReviewManager.ts +355 -0
- package/src/review/ReviewTimingManager.ts +265 -0
- package/src/survey-core/types.ts +244 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/types.ts +680 -0
- package/src/utils/ApiClient.ts +163 -0
- package/src/utils/CSSConverter.ts +552 -0
- package/src/utils/animations.ts +232 -0
- package/src/utils/calendarIntegration.ts +113 -0
- package/src/utils/configurationResolver.ts +106 -0
- package/src/utils/deviceDetection.ts +83 -0
- package/src/utils/hapticFeedback.ts +55 -0
- package/src/utils/iconMapper.ts +102 -0
- package/src/utils/notificationPermissions.ts +48 -0
- package/src/utils/priceFormatting.ts +119 -0
- package/src/utils/shareSheet.ts +55 -0
- package/src/utils/subscriptionUpgrade.ts +183 -0
- package/src/valueUtils.ts +108 -0
- 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
|
+
}
|