@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,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaywallCache - Smart caching layer for React Native
|
|
3
|
+
* Uses AsyncStorage for metadata and RNFS for asset caching
|
|
4
|
+
* Supports server-provided cache configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
8
|
+
import RNFS from 'react-native-fs';
|
|
9
|
+
|
|
10
|
+
export interface CacheConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
maxCacheAge: number; // milliseconds
|
|
13
|
+
backgroundUpdateEnabled: boolean;
|
|
14
|
+
preloadAssets: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_CACHE_CONFIG: CacheConfig = {
|
|
18
|
+
enabled: true,
|
|
19
|
+
maxCacheAge: 604800000, // 7 days
|
|
20
|
+
backgroundUpdateEnabled: true,
|
|
21
|
+
preloadAssets: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface CachedPaywall {
|
|
25
|
+
paywall: {
|
|
26
|
+
id: string;
|
|
27
|
+
identifier: string;
|
|
28
|
+
name: string;
|
|
29
|
+
configuration: any;
|
|
30
|
+
version: number;
|
|
31
|
+
};
|
|
32
|
+
version: number;
|
|
33
|
+
cachedAt: number;
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
etag?: string;
|
|
36
|
+
isFallback?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Track background updates in progress
|
|
40
|
+
const backgroundUpdateInProgress = new Set<string>();
|
|
41
|
+
|
|
42
|
+
export class PaywallCache {
|
|
43
|
+
private config: CacheConfig;
|
|
44
|
+
private cacheDir: string;
|
|
45
|
+
private fallbackCache: Map<string, CachedPaywall> = new Map();
|
|
46
|
+
|
|
47
|
+
constructor(config: Partial<CacheConfig> = {}) {
|
|
48
|
+
this.config = { ...DEFAULT_CACHE_CONFIG, ...config };
|
|
49
|
+
this.cacheDir = `${RNFS.CachesDirectoryPath}/billdog_paywalls`;
|
|
50
|
+
|
|
51
|
+
// Create cache directory
|
|
52
|
+
RNFS.mkdir(this.cacheDir).catch(() => {
|
|
53
|
+
// Directory might already exist
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// MARK: - Configuration
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update cache configuration from server response
|
|
61
|
+
*/
|
|
62
|
+
configureFromServer(serverConfig: CacheConfig): void {
|
|
63
|
+
this.config = { ...this.config, ...serverConfig };
|
|
64
|
+
console.log('[PaywallCache] Configured from server:', this.config);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get current cache configuration
|
|
69
|
+
*/
|
|
70
|
+
getConfig(): CacheConfig {
|
|
71
|
+
return { ...this.config };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// MARK: - Paywall Caching
|
|
75
|
+
|
|
76
|
+
async get(identifier: string): Promise<CachedPaywall | null> {
|
|
77
|
+
if (!this.config.enabled) {
|
|
78
|
+
// Still return fallback if available
|
|
79
|
+
return this.fallbackCache.get(identifier) || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const key = `paywall:${identifier}`;
|
|
84
|
+
const cachedJson = await AsyncStorage.getItem(key);
|
|
85
|
+
|
|
86
|
+
if (!cachedJson) {
|
|
87
|
+
console.log(`[PaywallCache] MISS: ${identifier}`);
|
|
88
|
+
// Return fallback if available
|
|
89
|
+
return this.fallbackCache.get(identifier) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cached: CachedPaywall = JSON.parse(cachedJson);
|
|
93
|
+
|
|
94
|
+
// Check expiration (unless it's a fallback)
|
|
95
|
+
if (!cached.isFallback && Date.now() > cached.expiresAt) {
|
|
96
|
+
console.log(`[PaywallCache] EXPIRED: ${identifier}`);
|
|
97
|
+
await this.delete(identifier);
|
|
98
|
+
// Return fallback if available
|
|
99
|
+
return this.fallbackCache.get(identifier) || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`[PaywallCache] HIT: ${identifier} (v${cached.version})${cached.isFallback ? ' (fallback)' : ''}`);
|
|
103
|
+
return cached;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(`[PaywallCache] ERROR reading cache:`, error);
|
|
106
|
+
return this.fallbackCache.get(identifier) || null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async set(identifier: string, paywall: CachedPaywall): Promise<void> {
|
|
111
|
+
if (!this.config.enabled && !paywall.isFallback) return;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Calculate expiration using server-provided maxCacheAge
|
|
115
|
+
const cacheEntry: CachedPaywall = {
|
|
116
|
+
...paywall,
|
|
117
|
+
cachedAt: Date.now(),
|
|
118
|
+
expiresAt: Date.now() + this.config.maxCacheAge,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const key = `paywall:${identifier}`;
|
|
122
|
+
const cachedJson = JSON.stringify(cacheEntry);
|
|
123
|
+
await AsyncStorage.setItem(key, cachedJson);
|
|
124
|
+
console.log(`[PaywallCache] CACHED: ${identifier} (v${paywall.version}) TTL: ${this.config.maxCacheAge / 1000}s`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`[PaywallCache] ERROR writing cache:`, error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Set a fallback paywall that will never be cleared
|
|
132
|
+
*/
|
|
133
|
+
async setFallback(identifier: string, paywall: CachedPaywall): Promise<void> {
|
|
134
|
+
const fallbackPaywall: CachedPaywall = {
|
|
135
|
+
...paywall,
|
|
136
|
+
isFallback: true,
|
|
137
|
+
expiresAt: Date.now() + (365 * 24 * 60 * 60 * 1000), // 1 year
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Store in memory for fast access
|
|
141
|
+
this.fallbackCache.set(identifier, fallbackPaywall);
|
|
142
|
+
|
|
143
|
+
// Also persist to storage
|
|
144
|
+
await this.set(identifier, fallbackPaywall);
|
|
145
|
+
console.log(`[PaywallCache] FALLBACK SET: ${identifier}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async delete(identifier: string, includeFallback: boolean = false): Promise<void> {
|
|
149
|
+
try {
|
|
150
|
+
// Check if it's a fallback and skip if not forcing
|
|
151
|
+
if (!includeFallback) {
|
|
152
|
+
const key = `paywall:${identifier}`;
|
|
153
|
+
const cachedJson = await AsyncStorage.getItem(key);
|
|
154
|
+
if (cachedJson) {
|
|
155
|
+
const cached: CachedPaywall = JSON.parse(cachedJson);
|
|
156
|
+
if (cached.isFallback) {
|
|
157
|
+
console.log(`[PaywallCache] SKIP DELETE FALLBACK: ${identifier}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const key = `paywall:${identifier}`;
|
|
164
|
+
await AsyncStorage.removeItem(key);
|
|
165
|
+
|
|
166
|
+
if (includeFallback) {
|
|
167
|
+
this.fallbackCache.delete(identifier);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`[PaywallCache] DELETED: ${identifier}`);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(`[PaywallCache] ERROR deleting cache:`, error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// MARK: - Background Updates
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if background update is needed and trigger if enabled
|
|
180
|
+
*/
|
|
181
|
+
shouldTriggerBackgroundUpdate(identifier: string): boolean {
|
|
182
|
+
if (!this.config.backgroundUpdateEnabled) return false;
|
|
183
|
+
if (backgroundUpdateInProgress.has(identifier)) return false;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
markBackgroundUpdateStarted(identifier: string): void {
|
|
188
|
+
backgroundUpdateInProgress.add(identifier);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
markBackgroundUpdateCompleted(identifier: string): void {
|
|
192
|
+
backgroundUpdateInProgress.delete(identifier);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MARK: - Asset Caching
|
|
196
|
+
|
|
197
|
+
async cacheAsset(url: string): Promise<string | null> {
|
|
198
|
+
if (!this.config.preloadAssets) return null;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const fileName = url.split('/').pop() || 'asset';
|
|
202
|
+
const localPath = `${this.cacheDir}/${fileName}`;
|
|
203
|
+
|
|
204
|
+
// Check if already cached
|
|
205
|
+
const exists = await RNFS.exists(localPath);
|
|
206
|
+
if (exists) {
|
|
207
|
+
console.log(`[PaywallCache] Asset already cached: ${fileName}`);
|
|
208
|
+
return localPath;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Download and cache
|
|
212
|
+
const result = await RNFS.downloadFile({
|
|
213
|
+
fromUrl: url,
|
|
214
|
+
toFile: localPath,
|
|
215
|
+
}).promise;
|
|
216
|
+
|
|
217
|
+
if (result.statusCode === 200) {
|
|
218
|
+
console.log(`[PaywallCache] Asset cached: ${fileName}`);
|
|
219
|
+
return localPath;
|
|
220
|
+
} else {
|
|
221
|
+
console.error(`[PaywallCache] Asset download failed: ${result.statusCode}`);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error(`[PaywallCache] Asset caching error:`, error);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Preload all assets from a paywall configuration
|
|
232
|
+
*/
|
|
233
|
+
async preloadPaywallAssets(configuration: any): Promise<void> {
|
|
234
|
+
if (!this.config.preloadAssets) return;
|
|
235
|
+
|
|
236
|
+
const assetUrls = this.extractAssetUrls(configuration);
|
|
237
|
+
console.log(`[PaywallCache] Preloading ${assetUrls.length} assets`);
|
|
238
|
+
|
|
239
|
+
await Promise.all(assetUrls.map(url => this.cacheAsset(url)));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private extractAssetUrls(config: any): string[] {
|
|
243
|
+
const urls: string[] = [];
|
|
244
|
+
|
|
245
|
+
const traverse = (obj: any) => {
|
|
246
|
+
if (!obj || typeof obj !== 'object') return;
|
|
247
|
+
|
|
248
|
+
if (typeof obj.src === 'string' && obj.src.startsWith('http')) {
|
|
249
|
+
urls.push(obj.src);
|
|
250
|
+
}
|
|
251
|
+
if (typeof obj.imageUrl === 'string' && obj.imageUrl.startsWith('http')) {
|
|
252
|
+
urls.push(obj.imageUrl);
|
|
253
|
+
}
|
|
254
|
+
if (typeof obj.backgroundImage === 'string' && obj.backgroundImage.startsWith('http')) {
|
|
255
|
+
urls.push(obj.backgroundImage);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Object.values(obj).forEach(traverse);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
traverse(config);
|
|
262
|
+
return [...new Set(urls)]; // Remove duplicates
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async getCachedAssetPath(url: string): Promise<string | null> {
|
|
266
|
+
try {
|
|
267
|
+
const fileName = url.split('/').pop() || 'asset';
|
|
268
|
+
const localPath = `${this.cacheDir}/${fileName}`;
|
|
269
|
+
|
|
270
|
+
const exists = await RNFS.exists(localPath);
|
|
271
|
+
return exists ? localPath : null;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error(`[PaywallCache] Error checking cached asset:`, error);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// MARK: - Cache Management
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Clear regular cache (preserves fallbacks)
|
|
282
|
+
*/
|
|
283
|
+
async clearAll(): Promise<void> {
|
|
284
|
+
try {
|
|
285
|
+
// Clear AsyncStorage keys (skip fallbacks)
|
|
286
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
287
|
+
const paywallKeys = keys.filter((key) => key.startsWith('paywall:'));
|
|
288
|
+
|
|
289
|
+
for (const key of paywallKeys) {
|
|
290
|
+
const cachedJson = await AsyncStorage.getItem(key);
|
|
291
|
+
if (cachedJson) {
|
|
292
|
+
const cached: CachedPaywall = JSON.parse(cachedJson);
|
|
293
|
+
if (!cached.isFallback) {
|
|
294
|
+
await AsyncStorage.removeItem(key);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Clear file cache
|
|
300
|
+
const files = await RNFS.readDir(this.cacheDir);
|
|
301
|
+
await Promise.all(files.map((file) => RNFS.unlink(file.path)));
|
|
302
|
+
|
|
303
|
+
console.log('[PaywallCache] Cache cleared (fallbacks preserved)');
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('[PaywallCache] Error clearing cache:', error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Clear all cache including fallbacks
|
|
311
|
+
*/
|
|
312
|
+
async clearAllIncludingFallbacks(): Promise<void> {
|
|
313
|
+
try {
|
|
314
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
315
|
+
const paywallKeys = keys.filter((key) => key.startsWith('paywall:'));
|
|
316
|
+
await AsyncStorage.multiRemove(paywallKeys);
|
|
317
|
+
|
|
318
|
+
const files = await RNFS.readDir(this.cacheDir);
|
|
319
|
+
await Promise.all(files.map((file) => RNFS.unlink(file.path)));
|
|
320
|
+
|
|
321
|
+
this.fallbackCache.clear();
|
|
322
|
+
|
|
323
|
+
console.log('[PaywallCache] All cache cleared including fallbacks');
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error('[PaywallCache] Error clearing cache:', error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async getStats(): Promise<{
|
|
330
|
+
paywallCount: number;
|
|
331
|
+
assetCount: number;
|
|
332
|
+
fallbackCount: number;
|
|
333
|
+
config: CacheConfig;
|
|
334
|
+
}> {
|
|
335
|
+
try {
|
|
336
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
337
|
+
const paywallKeys = keys.filter((key) => key.startsWith('paywall:'));
|
|
338
|
+
|
|
339
|
+
const files = await RNFS.readDir(this.cacheDir);
|
|
340
|
+
const assetCount = files.length;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
paywallCount: paywallKeys.length,
|
|
344
|
+
assetCount,
|
|
345
|
+
fallbackCount: this.fallbackCache.size,
|
|
346
|
+
config: this.config,
|
|
347
|
+
};
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('[PaywallCache] Error getting stats:', error);
|
|
350
|
+
return {
|
|
351
|
+
paywallCount: 0,
|
|
352
|
+
assetCount: 0,
|
|
353
|
+
fallbackCount: 0,
|
|
354
|
+
config: this.config,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaywallStateManager — element state for React Native paywall components.
|
|
3
|
+
*
|
|
4
|
+
* Parity with `io.billdog.compose.ui.PaywallStateManager` (Android) and
|
|
5
|
+
* `PaywallStateManager.shared` (iOS): a key→value store with per-key subscriptions,
|
|
6
|
+
* following the `element:{componentId}.{property}` convention, plus the paywall-wide
|
|
7
|
+
* `products.selectedIndex` key that drives selection-reactive Dynamic Values.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS EXISTS: 27 RN component files already called a `stateManager` prop
|
|
10
|
+
* (`updateState`, `subscribeToState`, `getState`, `setState`, `subscribe`) — but
|
|
11
|
+
* nothing ever PROVIDED one, and every call site optional-chains
|
|
12
|
+
* (`stateManager?.updateState?.(...)`). So the entire cross-component state protocol
|
|
13
|
+
* was dead code that failed silently: toggles could not drive visibility, tabs could
|
|
14
|
+
* not be driven externally, and product selection was invisible outside the products
|
|
15
|
+
* component. This is the missing implementation, not a new abstraction.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* PaywallStateManager.shared.setState('element:slider_1.currentValue', 75);
|
|
19
|
+
* const v = PaywallStateManager.shared.getState<number>('element:slider_1.currentValue');
|
|
20
|
+
*/
|
|
21
|
+
import { isDeepEqual } from './valueUtils';
|
|
22
|
+
|
|
23
|
+
export type StateListener = (value: any) => void;
|
|
24
|
+
export type Unsubscribe = () => void;
|
|
25
|
+
|
|
26
|
+
export class PaywallStateManager {
|
|
27
|
+
/**
|
|
28
|
+
* Paywall-wide key holding the live product selection.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately NOT `element:{id}.selectedIndex` — that form is scoped to a single
|
|
31
|
+
* products component, while Dynamic Values need one well-known key to react to. The
|
|
32
|
+
* name matches the variable key the DV bag emits (`products.selectedIndex`), so an
|
|
33
|
+
* authored condition and the state driving it are spelled identically.
|
|
34
|
+
*
|
|
35
|
+
* Parity: Android `PaywallStateManager.SELECTED_INDEX_KEY`, iOS
|
|
36
|
+
* `PaywallView.selectedIndexStateKey`.
|
|
37
|
+
*/
|
|
38
|
+
static readonly SELECTED_INDEX_KEY = 'products.selectedIndex';
|
|
39
|
+
|
|
40
|
+
/** Shared instance. Native uses a singleton (Kotlin `object` / Swift `.shared`); the
|
|
41
|
+
* class stays exported so tests can build an isolated store. */
|
|
42
|
+
static readonly shared = new PaywallStateManager();
|
|
43
|
+
|
|
44
|
+
private state = new Map<string, any>();
|
|
45
|
+
private listeners = new Map<string, Set<StateListener>>();
|
|
46
|
+
|
|
47
|
+
/** Read the current value, or `defaultValue` when unset. */
|
|
48
|
+
getState<T = any>(key: string, defaultValue?: T): T | undefined {
|
|
49
|
+
const v = this.state.get(key);
|
|
50
|
+
return v === undefined ? defaultValue : (v as T);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Write a value and notify subscribers.
|
|
55
|
+
*
|
|
56
|
+
* The structural-equality guard is load-bearing, not an optimisation: components
|
|
57
|
+
* write their state in an effect keyed on that same state, so an unconditional
|
|
58
|
+
* notify would loop (write → notify → setState → effect → write). Android relies on
|
|
59
|
+
* the identical guard.
|
|
60
|
+
*/
|
|
61
|
+
setState(key: string, value: any): void {
|
|
62
|
+
if (this.state.has(key) && isDeepEqual(this.state.get(key), value)) return;
|
|
63
|
+
this.state.set(key, value);
|
|
64
|
+
this.notify(key, value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Alias for {@link setState} — the name 53 existing call sites use (web-builder parity). */
|
|
68
|
+
updateState(key: string, value: any): void {
|
|
69
|
+
this.setState(key, value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Alias for {@link getState} — used by the component test suite. */
|
|
73
|
+
getValue<T = any>(key: string, defaultValue?: T): T | undefined {
|
|
74
|
+
return this.getState<T>(key, defaultValue);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Alias for {@link setState} — used by the component test suite. */
|
|
78
|
+
setValue(key: string, value: any): void {
|
|
79
|
+
this.setState(key, value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe to a key. Returns an unsubscribe function — components return it
|
|
84
|
+
* directly from `useEffect`, so it MUST be callable and idempotent.
|
|
85
|
+
*/
|
|
86
|
+
subscribe(key: string, listener: StateListener): Unsubscribe {
|
|
87
|
+
let set = this.listeners.get(key);
|
|
88
|
+
if (!set) {
|
|
89
|
+
set = new Set();
|
|
90
|
+
this.listeners.set(key, set);
|
|
91
|
+
}
|
|
92
|
+
set.add(listener);
|
|
93
|
+
return () => {
|
|
94
|
+
const current = this.listeners.get(key);
|
|
95
|
+
if (!current) return;
|
|
96
|
+
current.delete(listener);
|
|
97
|
+
if (current.size === 0) this.listeners.delete(key);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Alias for {@link subscribe} — the name 14 existing call sites use. */
|
|
102
|
+
subscribeToState(key: string, listener: StateListener): Unsubscribe {
|
|
103
|
+
return this.subscribe(key, listener);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear all state AND subscriptions, for a fresh paywall presentation.
|
|
108
|
+
*
|
|
109
|
+
* ORDERING TRAP (React-specific, inverted from the native SDKs): child effects run
|
|
110
|
+
* BEFORE parent effects, so calling this from a mount effect in PaywallView would
|
|
111
|
+
* wipe the subscriptions its children registered moments earlier, leaving every
|
|
112
|
+
* component permanently deaf. PaywallView therefore resets during its first render —
|
|
113
|
+
* before any child mounts. See the ref-guarded reset there.
|
|
114
|
+
*/
|
|
115
|
+
clearAll(): void {
|
|
116
|
+
this.state.clear();
|
|
117
|
+
this.listeners.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Clear state for one component (`element:{componentId}.*`). */
|
|
121
|
+
clearComponent(componentId: string): void {
|
|
122
|
+
const prefix = `element:${componentId}.`;
|
|
123
|
+
for (const key of Array.from(this.state.keys())) {
|
|
124
|
+
if (key.startsWith(prefix)) {
|
|
125
|
+
this.state.delete(key);
|
|
126
|
+
this.listeners.delete(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Snapshot of all state (debugging / diagnostics). */
|
|
132
|
+
getAllState(): Record<string, any> {
|
|
133
|
+
return Object.fromEntries(this.state);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private notify(key: string, value: any): void {
|
|
137
|
+
const set = this.listeners.get(key);
|
|
138
|
+
if (!set) return;
|
|
139
|
+
// Copy before iterating: a listener may unsubscribe (or subscribe) while running,
|
|
140
|
+
// which would otherwise mutate the Set mid-iteration.
|
|
141
|
+
for (const listener of Array.from(set)) {
|
|
142
|
+
try {
|
|
143
|
+
listener(value);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// One misbehaving component must not take down the paywall render or starve
|
|
146
|
+
// the remaining listeners of this update.
|
|
147
|
+
console.warn(`[BillDog] state listener for "${key}" threw:`, err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Shared instance — the one PaywallView threads through the component tree. */
|
|
154
|
+
export const paywallStateManager = PaywallStateManager.shared;
|