@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
package/src/BillDog.ts
ADDED
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BillDog — top-level RN facade.
|
|
3
|
+
*
|
|
4
|
+
* Spec: PUR-063, PUR-064, PUR-080..085, PUR-121, PUR-122, PUR-140..146,
|
|
5
|
+
* PUR-224, PUR-350, PUR-360, PUR-362, PUR-363, PUR-364
|
|
6
|
+
* (.lovable/purchase-conformance-spec.md)
|
|
7
|
+
* Standard: mem://architecture/react-native/sdk-unified-standard
|
|
8
|
+
* mem://architecture/customer-info-cache-standard
|
|
9
|
+
* mem://architecture/entitlements-authoritative-standard
|
|
10
|
+
* mem://architecture/purchase-conformance-standard
|
|
11
|
+
*
|
|
12
|
+
* Slice J (PUR-364) propagates the v1.4 purchase parity surface to the RN
|
|
13
|
+
* bridge with iOS/Android wire parity:
|
|
14
|
+
* - `purchase(params)` — typed PurchaseParams, returns canonical PurchaseResult
|
|
15
|
+
* - `purchase(custom)` — Superwall-style non-StoreKit products via host controller
|
|
16
|
+
* - `setPurchaseController(...)` — JS-side controller for EXTERNAL/CUSTOM flows
|
|
17
|
+
* - `setProxyURL` / `getProxyURL` — self-hosted proxy
|
|
18
|
+
* - `canMakePayments`, `presentCodeRedemptionSheet`,
|
|
19
|
+
* `checkTrialOrIntroDiscountEligibility`, `onStorefrontChange`
|
|
20
|
+
* - 14-provider attribution matrix + `collectDeviceIdentifiers`
|
|
21
|
+
*
|
|
22
|
+
* Every method gracefully falls back when `NativeModules.BillDog` is null so
|
|
23
|
+
* consumer apps compile against the contract before the native bridge is wired
|
|
24
|
+
* in their host project. The facade NEVER throws across the public boundary —
|
|
25
|
+
* store/network errors are surfaced as typed result unions.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { RestoreResult } from './models/RestoreResult';
|
|
29
|
+
import type { CacheFetchPolicy } from './models/CacheFetchPolicy';
|
|
30
|
+
import { DEFAULT_CACHE_FETCH_POLICY } from './models/CacheFetchPolicy';
|
|
31
|
+
import {
|
|
32
|
+
type CustomerInfo,
|
|
33
|
+
entitlementsByProductId,
|
|
34
|
+
} from './models/CustomerInfo';
|
|
35
|
+
import type { EntitlementInfo } from './models/EntitlementInfo';
|
|
36
|
+
import type { PurchaseParams } from './models/PurchaseParams';
|
|
37
|
+
import { PurchaseParams as PurchaseParamsNS } from './models/PurchaseParams';
|
|
38
|
+
import type { PurchaseResult } from './models/PurchaseResult';
|
|
39
|
+
import type { CustomProduct } from './models/CustomProduct';
|
|
40
|
+
import type {
|
|
41
|
+
PlacementResult,
|
|
42
|
+
PresentationResult,
|
|
43
|
+
PaywallPresentationHandler,
|
|
44
|
+
} from './models/Placement';
|
|
45
|
+
import type { BillDogDelegate } from './models/Delegate';
|
|
46
|
+
import { AnalyticsManager } from './modules/Analytics';
|
|
47
|
+
import { BillDogConfiguration } from './BillDogConfiguration';
|
|
48
|
+
import { ApiClient } from './utils/ApiClient';
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
51
|
+
const RN = (() => {
|
|
52
|
+
try {
|
|
53
|
+
return require('react-native');
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
|
|
59
|
+
const native: any = RN?.NativeModules?.BillDog ?? null;
|
|
60
|
+
|
|
61
|
+
// Optional AsyncStorage for persisting currentUserId across launches.
|
|
62
|
+
// Falls back to in-memory if the host app hasn't installed it.
|
|
63
|
+
const AsyncStorage: any = (() => {
|
|
64
|
+
try {
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
66
|
+
return require('@react-native-async-storage/async-storage').default;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
// -----------------------------------------------------------------------------
|
|
73
|
+
// Identity API — Android parity (BillDog.kt: logIn, logOut, identify,
|
|
74
|
+
// isAnonymous, getAppUserId, isConfigured, setSandboxMode).
|
|
75
|
+
// Persisted across launches via AsyncStorage when available.
|
|
76
|
+
// -----------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
const CURRENT_USER_KEY = 'billdog.rn.currentUserId';
|
|
79
|
+
let _currentUserId: string | null = null;
|
|
80
|
+
|
|
81
|
+
// Rehydrate from storage on module load — best-effort, never throws.
|
|
82
|
+
if (AsyncStorage?.getItem) {
|
|
83
|
+
AsyncStorage.getItem(CURRENT_USER_KEY)
|
|
84
|
+
.then((id: string | null) => {
|
|
85
|
+
if (id && !_currentUserId) _currentUserId = id;
|
|
86
|
+
})
|
|
87
|
+
.catch(() => {
|
|
88
|
+
/* ignore — degrade to in-memory */
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* PUR-142/224: multi-subscriber CustomerInfo emitter with replay-1 semantics.
|
|
94
|
+
* New subscribers immediately receive the most recent cached value (if any).
|
|
95
|
+
*/
|
|
96
|
+
type CustomerInfoListener = (info: CustomerInfo) => void;
|
|
97
|
+
|
|
98
|
+
class CustomerInfoEmitter {
|
|
99
|
+
private listeners = new Set<CustomerInfoListener>();
|
|
100
|
+
private lastValue: CustomerInfo | null = null;
|
|
101
|
+
|
|
102
|
+
addListener(fn: CustomerInfoListener): { remove: () => void } {
|
|
103
|
+
this.listeners.add(fn);
|
|
104
|
+
if (this.lastValue) fn(this.lastValue);
|
|
105
|
+
return { remove: () => this.listeners.delete(fn) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
emit(info: CustomerInfo): void {
|
|
109
|
+
this.lastValue = info;
|
|
110
|
+
for (const l of this.listeners) l(info);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get latest(): CustomerInfo | null {
|
|
114
|
+
return this.lastValue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const customerInfoEmitter = new CustomerInfoEmitter();
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* PUR-364: Storefront country observer (iOS-only on the producer side; the
|
|
122
|
+
* RN bridge surfaces a no-op on Android until Play exposes a country API).
|
|
123
|
+
*/
|
|
124
|
+
type StorefrontListener = (storefront: { countryCode: string }) => void;
|
|
125
|
+
const storefrontListeners = new Set<StorefrontListener>();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* PUR-364: JS-side PurchaseController contract — mirrors iOS/Android.
|
|
129
|
+
* Implementations MUST map user cancellation to `{type:'cancelled'}` and
|
|
130
|
+
* Ask-to-Buy / Android PENDING to `{type:'pending'}` — never `failed`.
|
|
131
|
+
*/
|
|
132
|
+
export interface PurchaseController {
|
|
133
|
+
purchase(productId: string): Promise<PurchaseResult>;
|
|
134
|
+
restorePurchases(): Promise<RestoreResult>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let activeController: PurchaseController | null = null;
|
|
138
|
+
let activeDelegate: BillDogDelegate | null = null;
|
|
139
|
+
|
|
140
|
+
/** Stamp every receipt posted to BillDog's backend (PUR-185). */
|
|
141
|
+
const sourceForResult = (): 'INTERNAL' | 'EXTERNAL' =>
|
|
142
|
+
activeController ? 'EXTERNAL' : 'INTERNAL';
|
|
143
|
+
|
|
144
|
+
const cancelledResult: PurchaseResult = { type: 'cancelled' };
|
|
145
|
+
|
|
146
|
+
const failedResult = (e: any, category: 'unknown' | 'network' = 'unknown'): PurchaseResult => ({
|
|
147
|
+
type: 'failed',
|
|
148
|
+
error: { category, message: e?.message ?? 'Purchase failed' },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Best-effort POST of an EXTERNAL (host-controller) custom-product purchase
|
|
153
|
+
* to BillDog's `record-external-purchase` endpoint, which resolves the
|
|
154
|
+
* catalog product from `providerProductId`, grants the entitlement, and
|
|
155
|
+
* reports revenue with `source = EXTERNAL`.
|
|
156
|
+
*/
|
|
157
|
+
async function recordExternalCustomPurchase(
|
|
158
|
+
product: CustomProduct,
|
|
159
|
+
transactionId?: string,
|
|
160
|
+
transactionType: string = 'initial_purchase'
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const userId = BillDog.getAppUserId();
|
|
163
|
+
if (!userId) {
|
|
164
|
+
console.warn('[BD-EXTERNAL] Skipping record — app user id unresolved');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const txId = transactionId || `ext_${Math.random().toString(36).substring(2)}${Date.now()}`;
|
|
168
|
+
const body = {
|
|
169
|
+
user_id: userId,
|
|
170
|
+
transaction_id: txId,
|
|
171
|
+
product_id: product.id,
|
|
172
|
+
provider: product.provider,
|
|
173
|
+
provider_product_id: product.providerProductId,
|
|
174
|
+
purchase_date: new Date().toISOString(),
|
|
175
|
+
store: 'external',
|
|
176
|
+
price: product.price,
|
|
177
|
+
currency: product.currency,
|
|
178
|
+
transaction_type: transactionType,
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
await ApiClient.post('record-external-purchase', body);
|
|
182
|
+
console.log(`[BD-EXTERNAL] Recorded custom purchase '${product.id}' (provider=${product.provider})`);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn(`[BD-EXTERNAL] record-external-purchase failed (not blocking):`, e);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** SDK version constant — Android parity (BillDog.kt:51). */
|
|
189
|
+
export const SDK_VERSION = '1.0.0-beta.2';
|
|
190
|
+
|
|
191
|
+
export const BillDog = {
|
|
192
|
+
/** SDK version constant — re-exposed on the singleton for ergonomic access. */
|
|
193
|
+
SDK_VERSION,
|
|
194
|
+
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
// PUR-350 / PUR-364 — Unified purchase API
|
|
197
|
+
// -------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Purchase a product with typed `PurchaseParams`. Returns canonical
|
|
201
|
+
* `PurchaseResult` — NEVER throws across the public boundary.
|
|
202
|
+
*
|
|
203
|
+
* If a host `PurchaseController` is registered (see `setPurchaseController`),
|
|
204
|
+
* the controller drives the transaction and the receipt is stamped
|
|
205
|
+
* `source = EXTERNAL`. Otherwise the bundled native controller is used
|
|
206
|
+
* (`source = INTERNAL`).
|
|
207
|
+
*/
|
|
208
|
+
async purchase(params: PurchaseParams): Promise<PurchaseResult> {
|
|
209
|
+
if (activeController) {
|
|
210
|
+
try {
|
|
211
|
+
return await activeController.purchase(params.productId);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return failedResult(e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!native?.purchase) {
|
|
217
|
+
// Bridge not wired in host app — return cancelled so the UI is consistent.
|
|
218
|
+
return cancelledResult;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const raw = await native.purchase({ ...params, source: sourceForResult() });
|
|
222
|
+
if (raw && typeof raw.type === 'string') return raw as PurchaseResult;
|
|
223
|
+
return cancelledResult;
|
|
224
|
+
} catch (e) {
|
|
225
|
+
return failedResult(e);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* PUR-363: Purchase a Superwall-style non-StoreKit / non-Play product.
|
|
231
|
+
* Routing is ONLY through a host-provided PurchaseController.
|
|
232
|
+
*/
|
|
233
|
+
async purchaseCustom(product: CustomProduct): Promise<PurchaseResult> {
|
|
234
|
+
if (!activeController) {
|
|
235
|
+
return failedResult(
|
|
236
|
+
new Error('CustomProduct requires a host PurchaseController. Call BillDog.setPurchaseController() first.'),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const raw = await activeController.purchase(product.providerProductId);
|
|
241
|
+
if (raw && raw.type === 'purchased') {
|
|
242
|
+
// Best-effort EXTERNAL recording so the entitlement is granted and revenue reported.
|
|
243
|
+
recordExternalCustomPurchase(product, raw.transactionId).catch(() => {});
|
|
244
|
+
BillDog.getCustomerInfo('FETCH_CURRENT').catch(() => {});
|
|
245
|
+
}
|
|
246
|
+
return raw;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return failedResult(e);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/** Get the currently-registered host PurchaseController, or null. */
|
|
253
|
+
getPurchaseController(): PurchaseController | null {
|
|
254
|
+
return activeController;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/** Flush pending analytics events to the backend. */
|
|
258
|
+
async flushAnalytics(): Promise<void> {
|
|
259
|
+
try { await AnalyticsManager.getInstance().flush(); } catch { /* best-effort */ }
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/** PUR-364: Register a host PurchaseController. Pass `null` to detach. */
|
|
263
|
+
setPurchaseController(controller: PurchaseController | null): void {
|
|
264
|
+
activeController = controller;
|
|
265
|
+
if (native?.setPurchaseControllerActive) {
|
|
266
|
+
try {
|
|
267
|
+
native.setPurchaseControllerActive(!!controller);
|
|
268
|
+
} catch {
|
|
269
|
+
/* bridge no-op */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Restore previous purchases (PUR-080).
|
|
276
|
+
*
|
|
277
|
+
* - `{type:'restored', items: []}` — nothing to restore
|
|
278
|
+
* - `{type:'restored', items: [...]}` — items resurfaced
|
|
279
|
+
* - `{type:'failed', error: BillDogError}` — global failure
|
|
280
|
+
*/
|
|
281
|
+
async restore(): Promise<RestoreResult> {
|
|
282
|
+
if (activeController) {
|
|
283
|
+
try {
|
|
284
|
+
return await activeController.restorePurchases();
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
return {
|
|
287
|
+
type: 'failed',
|
|
288
|
+
error: { category: 'unknown', message: e?.message ?? 'Restore failed' },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!native?.restore) {
|
|
293
|
+
return { type: 'restored', items: [] };
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const raw = await native.restore();
|
|
297
|
+
if (raw && (raw.type === 'restored' || raw.type === 'failed')) {
|
|
298
|
+
return raw as RestoreResult;
|
|
299
|
+
}
|
|
300
|
+
return { type: 'restored', items: [] };
|
|
301
|
+
} catch (e: any) {
|
|
302
|
+
return {
|
|
303
|
+
type: 'failed',
|
|
304
|
+
error: { category: 'unknown', message: e?.message ?? 'Restore failed' },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
// -------------------------------------------------------------------------
|
|
310
|
+
// PUR-141/224 — CustomerInfo
|
|
311
|
+
// -------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
async getCustomerInfo(
|
|
314
|
+
policy: CacheFetchPolicy = DEFAULT_CACHE_FETCH_POLICY,
|
|
315
|
+
): Promise<CustomerInfo | null> {
|
|
316
|
+
if (!native?.getCustomerInfo) {
|
|
317
|
+
return customerInfoEmitter.latest;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const info = (await native.getCustomerInfo(policy)) as CustomerInfo;
|
|
321
|
+
customerInfoEmitter.emit(info);
|
|
322
|
+
return info;
|
|
323
|
+
} catch {
|
|
324
|
+
return customerInfoEmitter.latest;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
customerInfoEmitter,
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* PUR-121: resolve entitlements unlocked by a [productId] from the local
|
|
332
|
+
* `productEntitlementMapping`. Used for instant gating.
|
|
333
|
+
*/
|
|
334
|
+
entitlementsForProduct(productId: string): EntitlementInfo[] {
|
|
335
|
+
const info = customerInfoEmitter.latest;
|
|
336
|
+
if (!info) return [];
|
|
337
|
+
return entitlementsByProductId(info, productId);
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
// -------------------------------------------------------------------------
|
|
341
|
+
// PUR-201 / PUR-202 / PUR-330 / PUR-037 — store capability surface
|
|
342
|
+
// -------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
/** PUR-201: Can the device transact at all? (parental controls, MDM) */
|
|
345
|
+
async canMakePayments(): Promise<boolean> {
|
|
346
|
+
if (!native?.canMakePayments) return true;
|
|
347
|
+
try {
|
|
348
|
+
return !!(await native.canMakePayments());
|
|
349
|
+
} catch {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/** PUR-330: Present the App Store promo code redemption sheet (iOS-only). */
|
|
355
|
+
async presentCodeRedemptionSheet(): Promise<void> {
|
|
356
|
+
if (!native?.presentCodeRedemptionSheet) return;
|
|
357
|
+
try {
|
|
358
|
+
await native.presentCodeRedemptionSheet();
|
|
359
|
+
} catch {
|
|
360
|
+
/* ignore */
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* PUR-037: Check trial / intro-discount eligibility for an array of product
|
|
366
|
+
* ids. Returns a map of `productId` -> `'eligible' | 'ineligible' | 'unknown'`.
|
|
367
|
+
*/
|
|
368
|
+
async checkTrialOrIntroDiscountEligibility(
|
|
369
|
+
productIds: string[],
|
|
370
|
+
): Promise<Record<string, 'eligible' | 'ineligible' | 'unknown'>> {
|
|
371
|
+
if (!native?.checkTrialOrIntroDiscountEligibility) {
|
|
372
|
+
return Object.fromEntries(productIds.map((id) => [id, 'unknown']));
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
return (await native.checkTrialOrIntroDiscountEligibility(productIds)) as Record<
|
|
376
|
+
string,
|
|
377
|
+
'eligible' | 'ineligible' | 'unknown'
|
|
378
|
+
>;
|
|
379
|
+
} catch {
|
|
380
|
+
return Object.fromEntries(productIds.map((id) => [id, 'unknown']));
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/** PUR-202: Subscribe to storefront country changes (iOS-only producer). */
|
|
385
|
+
onStorefrontChange(listener: StorefrontListener): { remove: () => void } {
|
|
386
|
+
storefrontListeners.add(listener);
|
|
387
|
+
if (native?.subscribeStorefront) {
|
|
388
|
+
try {
|
|
389
|
+
native.subscribeStorefront();
|
|
390
|
+
} catch {
|
|
391
|
+
/* ignore */
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { remove: () => storefrontListeners.delete(listener) };
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
// -------------------------------------------------------------------------
|
|
398
|
+
// PUR-362 — Self-hosted proxy
|
|
399
|
+
// -------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
setProxyURL(url: string | null): void {
|
|
402
|
+
if (!native?.setProxyURL) return;
|
|
403
|
+
try {
|
|
404
|
+
native.setProxyURL(url);
|
|
405
|
+
} catch {
|
|
406
|
+
/* ignore */
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Consent (cross-SDK contract) — delegates to the native ConsentManager when the
|
|
412
|
+
// host wires the BillDog native module, and degrades to a no-op otherwise (this
|
|
413
|
+
// package is fallback-by-design; full native enforcement needs the BillDog native
|
|
414
|
+
// module + bridge registration on both platforms — tracked as a separate task).
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
setConsentRequired(required: boolean): void {
|
|
417
|
+
if (!native?.setConsentRequired) return;
|
|
418
|
+
try {
|
|
419
|
+
native.setConsentRequired(required);
|
|
420
|
+
} catch {
|
|
421
|
+
/* ignore */
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
setConsent(purpose: 'analytics' | 'replay' | 'messaging' | 'survey', granted: boolean): void {
|
|
426
|
+
if (!native?.setConsent) return;
|
|
427
|
+
try {
|
|
428
|
+
native.setConsent(purpose, granted);
|
|
429
|
+
} catch {
|
|
430
|
+
/* ignore */
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
setConsentGiven(given: boolean): void {
|
|
435
|
+
if (!native?.setConsentGiven) return;
|
|
436
|
+
try {
|
|
437
|
+
native.setConsentGiven(given);
|
|
438
|
+
} catch {
|
|
439
|
+
/* ignore */
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
async consentHeaderValue(): Promise<string | null> {
|
|
444
|
+
if (!native?.consentHeaderValue) return null;
|
|
445
|
+
try {
|
|
446
|
+
return (await native.consentHeaderValue()) ?? null;
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
getProxyURL(): string | null {
|
|
453
|
+
if (!native?.getProxyURL) return null;
|
|
454
|
+
try {
|
|
455
|
+
return native.getProxyURL() ?? null;
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
// -------------------------------------------------------------------------
|
|
462
|
+
// PUR-360 — Attribution matrix (14 providers, EXACT parity with the native
|
|
463
|
+
// SDKs: BillDog.kt / BillDog.swift). Each convenience setter forwards the
|
|
464
|
+
// provider's attribution key to the native `setAttribution` bridge, which
|
|
465
|
+
// delegates to the matching consent-gated native setter.
|
|
466
|
+
//
|
|
467
|
+
// Two provider groups, both natively backed:
|
|
468
|
+
// - 11 "modern" providers routed through the native `setAttribution` map
|
|
469
|
+
// (CleverTap … Segment).
|
|
470
|
+
// - 3 MMP providers with dedicated native setters (Adjust, AppsFlyer,
|
|
471
|
+
// Firebase). They share the same JS/bridge funnel — the bridge maps their
|
|
472
|
+
// keys to `setAdjustId` / `setAppsflyerId` / `setFirebaseAppInstanceId`.
|
|
473
|
+
//
|
|
474
|
+
// NOTE: Branch, Amplitude and Singular are NOT supported natively and were
|
|
475
|
+
// removed (they were JS-only no-ops). `setIterableUserID` / `setFBAnonymousID`
|
|
476
|
+
// replace the previously-misnamed `setIterableID` / `setFirebaseAppInstanceID`.
|
|
477
|
+
// -------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Set one or more attribution ids at once. Keys are the provider attribution
|
|
481
|
+
* keys (e.g. `mixpanelDistinctId`, `cleverTapId`, `iterableId`). Prefer the
|
|
482
|
+
* named convenience setters below unless batching.
|
|
483
|
+
*/
|
|
484
|
+
setAttribution(data: Record<string, string>): void {
|
|
485
|
+
if (!native?.setAttribution) return;
|
|
486
|
+
try {
|
|
487
|
+
native.setAttribution(data);
|
|
488
|
+
} catch {
|
|
489
|
+
/* swallow — attribution is best-effort */
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
setCleverTapID: (id: string | null) => setAttributionKey('cleverTapId', id),
|
|
494
|
+
setMixpanelDistinctID: (id: string | null) => setAttributionKey('mixpanelDistinctId', id),
|
|
495
|
+
setOnesignalID: (id: string | null) => setAttributionKey('onesignalId', id),
|
|
496
|
+
setFBAnonymousID: (id: string | null) => setAttributionKey('fbAnonymousId', id),
|
|
497
|
+
setAirshipChannelID: (id: string | null) => setAttributionKey('airshipChannelId', id),
|
|
498
|
+
setMoEngageAnonymousID: (id: string | null) => setAttributionKey('moEngageAnonymousId', id),
|
|
499
|
+
setMparticleID: (id: string | null) => setAttributionKey('mparticleId', id),
|
|
500
|
+
setKochavaDeviceID: (id: string | null) => setAttributionKey('kochavaId', id),
|
|
501
|
+
setTenjinAnalyticsInstallationID: (id: string | null) => setAttributionKey('tenjinId', id),
|
|
502
|
+
setIterableUserID: (id: string | null) => setAttributionKey('iterableId', id),
|
|
503
|
+
setSegmentAnonymousID: (id: string | null) => setAttributionKey('segmentId', id),
|
|
504
|
+
|
|
505
|
+
// MMP providers — dedicated native setters, same JS/bridge funnel.
|
|
506
|
+
setAdjustId: (id: string | null) => setAttributionKey('adjustId', id),
|
|
507
|
+
setAppsflyerId: (id: string | null) => setAttributionKey('appsflyerId', id),
|
|
508
|
+
setFirebaseAppInstanceId: (id: string | null) => setAttributionKey('firebaseAppInstanceId', id),
|
|
509
|
+
|
|
510
|
+
/** Collect IDFA / GAID after consent has been granted. */
|
|
511
|
+
collectDeviceIdentifiers(): void {
|
|
512
|
+
if (!native?.collectDeviceIdentifiers) return;
|
|
513
|
+
try {
|
|
514
|
+
native.collectDeviceIdentifiers();
|
|
515
|
+
} catch {
|
|
516
|
+
/* ignore */
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
// -------------------------------------------------------------------------
|
|
521
|
+
// Identity API — Android parity (BillDog.kt:782/807/845/890/894/325)
|
|
522
|
+
// -------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Log in an app user. Persists the user id, propagates to the native bridge
|
|
526
|
+
* (if wired) and the analytics module. Throws on blank ids — mirrors KMP/Unity.
|
|
527
|
+
*/
|
|
528
|
+
async logIn(appUserId: string): Promise<void> {
|
|
529
|
+
if (!appUserId || !appUserId.trim()) {
|
|
530
|
+
throw new Error('appUserId must be non-blank');
|
|
531
|
+
}
|
|
532
|
+
_currentUserId = appUserId;
|
|
533
|
+
if (AsyncStorage?.setItem) {
|
|
534
|
+
try {
|
|
535
|
+
await AsyncStorage.setItem(CURRENT_USER_KEY, appUserId);
|
|
536
|
+
} catch {
|
|
537
|
+
/* ignore — degrade to in-memory */
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (native?.logIn) {
|
|
541
|
+
try {
|
|
542
|
+
await native.logIn(appUserId);
|
|
543
|
+
} catch {
|
|
544
|
+
/* swallow — identity is also tracked locally */
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await AnalyticsManager.getInstance().identify(appUserId);
|
|
549
|
+
} catch {
|
|
550
|
+
/* analytics is best-effort */
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
/** Log out the current user and return to anonymous state. */
|
|
555
|
+
async logOut(): Promise<void> {
|
|
556
|
+
_currentUserId = null;
|
|
557
|
+
if (AsyncStorage?.removeItem) {
|
|
558
|
+
try {
|
|
559
|
+
await AsyncStorage.removeItem(CURRENT_USER_KEY);
|
|
560
|
+
} catch {
|
|
561
|
+
/* ignore */
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (native?.logOut) {
|
|
565
|
+
try {
|
|
566
|
+
await native.logOut();
|
|
567
|
+
} catch {
|
|
568
|
+
/* swallow */
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
await AnalyticsManager.getInstance().reset();
|
|
573
|
+
} catch {
|
|
574
|
+
/* best-effort */
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Identify a user with optional attributes. Behaves as `logIn` plus an
|
|
580
|
+
* analytics super-properties update for the supplied attributes.
|
|
581
|
+
*/
|
|
582
|
+
async identify(userId: string, attributes?: Record<string, any>): Promise<void> {
|
|
583
|
+
await this.logIn(userId);
|
|
584
|
+
if (attributes && Object.keys(attributes).length > 0) {
|
|
585
|
+
try {
|
|
586
|
+
await AnalyticsManager.getInstance().setSuperProperties(attributes);
|
|
587
|
+
} catch {
|
|
588
|
+
/* analytics is best-effort */
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
/** True when no app user is currently logged in. */
|
|
594
|
+
isAnonymous(): boolean {
|
|
595
|
+
return !_currentUserId;
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
/** Returns the currently logged-in app user id, or null when anonymous. */
|
|
599
|
+
getAppUserId(): string | null {
|
|
600
|
+
return _currentUserId;
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Mirrors Android `BillDog.isConfigured()` (BillDog.kt:325). Delegates to
|
|
605
|
+
* the configuration singleton so the apiKey-presence check lives in one place.
|
|
606
|
+
*/
|
|
607
|
+
isConfigured(): boolean {
|
|
608
|
+
return BillDogConfiguration.isConfigured();
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Cross-SDK parity stub matching KMP / Unity / Android. Toggles a local
|
|
613
|
+
* flag and forwards to the native bridge if wired; otherwise a documented
|
|
614
|
+
* no-op (sandbox is primarily controlled via API key prefix).
|
|
615
|
+
*/
|
|
616
|
+
setSandboxMode(enabled: boolean): void {
|
|
617
|
+
if (native?.setSandboxMode) {
|
|
618
|
+
try {
|
|
619
|
+
native.setSandboxMode(enabled);
|
|
620
|
+
} catch {
|
|
621
|
+
/* ignore */
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
// -------------------------------------------------------------------------
|
|
627
|
+
// Placement registration — Android parity (BillDog.kt:981/990/1024/1938)
|
|
628
|
+
// -------------------------------------------------------------------------
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Register a placement and run the canonical placement flow: fetch config →
|
|
632
|
+
* check entitlement → present paywall if needed. Returns a `PlacementResult`
|
|
633
|
+
* describing the outcome. Optional `handler` receives lifecycle callbacks
|
|
634
|
+
* mirroring Android's `PaywallPresentationHandler`.
|
|
635
|
+
*
|
|
636
|
+
* Returns `{ type: 'entitled' }` immediately if the user already holds the
|
|
637
|
+
* placement's required entitlement (no paywall shown). Returns
|
|
638
|
+
* `{ type: 'error', message }` on any failure — never throws across the
|
|
639
|
+
* public boundary.
|
|
640
|
+
*/
|
|
641
|
+
async register(
|
|
642
|
+
placementId: string,
|
|
643
|
+
params?: Record<string, unknown>,
|
|
644
|
+
handler?: PaywallPresentationHandler,
|
|
645
|
+
): Promise<PlacementResult> {
|
|
646
|
+
if (!BillDogConfiguration.isConfigured()) {
|
|
647
|
+
const err = 'BillDog SDK not configured.';
|
|
648
|
+
handler?.onLoadingStateChange?.('failed');
|
|
649
|
+
handler?.onError?.(new Error(err));
|
|
650
|
+
handler?.onSkip?.('error');
|
|
651
|
+
return { type: 'error', message: err };
|
|
652
|
+
}
|
|
653
|
+
handler?.onLoadingStateChange?.('loading');
|
|
654
|
+
|
|
655
|
+
if (!native?.registerPlacement) {
|
|
656
|
+
// No native bridge — emit no-op success path so the contract is met.
|
|
657
|
+
handler?.onLoadingStateChange?.('ready');
|
|
658
|
+
handler?.onSkip?.('no_paywall_configured');
|
|
659
|
+
return { type: 'error', message: 'Native placement bridge not wired' };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
const raw = await native.registerPlacement({ placementId, params: params ?? {} });
|
|
664
|
+
const result = (raw && typeof raw.type === 'string'
|
|
665
|
+
? raw
|
|
666
|
+
: { type: 'error', message: 'Malformed placement result' }) as PlacementResult;
|
|
667
|
+
|
|
668
|
+
switch (result.type) {
|
|
669
|
+
case 'entitled':
|
|
670
|
+
handler?.onLoadingStateChange?.('ready');
|
|
671
|
+
handler?.onSkip?.('user_has_entitlement');
|
|
672
|
+
break;
|
|
673
|
+
case 'purchased':
|
|
674
|
+
case 'dismissed':
|
|
675
|
+
case 'restored':
|
|
676
|
+
handler?.onLoadingStateChange?.('ready');
|
|
677
|
+
handler?.onPresent?.(placementId);
|
|
678
|
+
if (result.type === 'purchased') {
|
|
679
|
+
handler?.onPurchaseSuccess?.(result.productId);
|
|
680
|
+
}
|
|
681
|
+
handler?.onDismiss?.(placementId);
|
|
682
|
+
handler?.onFinish?.(placementId, true);
|
|
683
|
+
break;
|
|
684
|
+
case 'error':
|
|
685
|
+
handler?.onLoadingStateChange?.('failed');
|
|
686
|
+
handler?.onError?.(new Error(result.message));
|
|
687
|
+
handler?.onSkip?.('error');
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
} catch (e: any) {
|
|
692
|
+
const msg = e?.message ?? 'Placement registration failed';
|
|
693
|
+
handler?.onLoadingStateChange?.('failed');
|
|
694
|
+
handler?.onError?.(new Error(msg));
|
|
695
|
+
handler?.onSkip?.('error');
|
|
696
|
+
return { type: 'error', message: msg };
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Synchronous probe — does the current user have access to a placement?
|
|
702
|
+
* Returns `true` when the placement is unregistered or has no required
|
|
703
|
+
* entitlement (safe default matching Android's behaviour).
|
|
704
|
+
*/
|
|
705
|
+
hasAccessForPlacement(placementId: string): boolean {
|
|
706
|
+
if (!native?.hasAccessForPlacement) return true;
|
|
707
|
+
try {
|
|
708
|
+
return !!native.hasAccessForPlacement(placementId);
|
|
709
|
+
} catch {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
// Experiments & feature flags: the paywall-facade shim was removed. Use the
|
|
715
|
+
// standalone A/B module (BillDogABTest / ABTestManager) instead.
|
|
716
|
+
|
|
717
|
+
// ── In-app messages — Android parity (BillDog.kt:1969) ────────────
|
|
718
|
+
|
|
719
|
+
/** Open the IAM inbox / present a single message. */
|
|
720
|
+
async showInAppMessages(placementId?: string): Promise<void> {
|
|
721
|
+
if (native?.showInAppMessages) {
|
|
722
|
+
try { await native.showInAppMessages(placementId ?? null); } catch { /* ignore */ }
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
// ── Deep linking — Android parity (BillDog.kt:684) ────────────────
|
|
727
|
+
|
|
728
|
+
/** Route a deep link through the SDK. Returns true when handled. */
|
|
729
|
+
async handleDeepLink(uri: string): Promise<boolean> {
|
|
730
|
+
if (native?.handleDeepLink) {
|
|
731
|
+
try { return !!(await native.handleDeepLink(uri)); } catch { /* ignore */ }
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
// ── Component selection state — Android parity (BillDog.kt:1032–1044) ──
|
|
737
|
+
|
|
738
|
+
getSelectedProductIndex(componentId: string): number | null {
|
|
739
|
+
if (native?.getSelectedProductIndex) {
|
|
740
|
+
try { return native.getSelectedProductIndex(componentId); } catch { /* ignore */ }
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
setSelectedProductIndex(componentId: string, index: number): void {
|
|
746
|
+
if (native?.setSelectedProductIndex) {
|
|
747
|
+
try { native.setSelectedProductIndex(componentId, index); } catch { /* ignore */ }
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
// ── Theme override — Android parity (BillDog.kt:184) ──────────────
|
|
752
|
+
|
|
753
|
+
/** Apply runtime theme overrides. Map of token name → value. */
|
|
754
|
+
updateTheme(overrides: Record<string, string>): void {
|
|
755
|
+
if (native?.updateTheme) {
|
|
756
|
+
try { native.updateTheme(overrides); } catch { /* ignore */ }
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
/** Set the SDK-wide interface style. Pass `null` to follow system. */
|
|
761
|
+
setInterfaceStyle(style: 'light' | 'dark' | null): void {
|
|
762
|
+
if (native?.setInterfaceStyle) {
|
|
763
|
+
try { native.setInterfaceStyle(style); } catch { /* ignore */ }
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
// ── Subscription management — Android parity (BillDog.kt:1500) ────
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Open the platform-native subscription management UI. iOS routes to
|
|
771
|
+
* the App Store; Android routes to Play Store; web throws since no
|
|
772
|
+
* platform UI is available.
|
|
773
|
+
*/
|
|
774
|
+
async showManageSubscriptions(subscriptionId?: string): Promise<void> {
|
|
775
|
+
if (native?.showManageSubscriptions) {
|
|
776
|
+
try { await native.showManageSubscriptions(subscriptionId ?? null); } catch { /* ignore */ }
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
// ── Cache control — Android parity (BillDog.kt:1074, :1612) ───────
|
|
781
|
+
|
|
782
|
+
/** Clear all SDK caches (paywall configs, customer info, products). */
|
|
783
|
+
async clearCache(): Promise<void> {
|
|
784
|
+
if (native?.clearCache) {
|
|
785
|
+
try { await native.clearCache(); } catch { /* ignore */ }
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
|
|
789
|
+
/** Invalidate the cached customer info — next read triggers a refetch. */
|
|
790
|
+
async invalidateCustomerInfoCache(): Promise<void> {
|
|
791
|
+
if (native?.invalidateCustomerInfoCache) {
|
|
792
|
+
try { await native.invalidateCustomerInfoCache(); } catch { /* ignore */ }
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
// ── User attributes — Android parity (BillDog.kt:1714, :1729) ─────
|
|
797
|
+
|
|
798
|
+
/** Set typed attributes on the current user (targeting + analytics). */
|
|
799
|
+
async setUserAttributes(attributes: Record<string, unknown>): Promise<void> {
|
|
800
|
+
if (native?.setUserAttributes) {
|
|
801
|
+
try { await native.setUserAttributes(attributes); } catch { /* ignore */ }
|
|
802
|
+
}
|
|
803
|
+
// Also propagate to AnalyticsManager so events carry the same attrs.
|
|
804
|
+
try {
|
|
805
|
+
await AnalyticsManager.getInstance().setSuperProperties(attributes);
|
|
806
|
+
} catch { /* best-effort */ }
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
/** Get the current user attributes (snapshot). */
|
|
810
|
+
async getUserAttributes(): Promise<Record<string, unknown>> {
|
|
811
|
+
if (native?.getUserAttributes) {
|
|
812
|
+
try { return (await native.getUserAttributes()) ?? {}; } catch { /* ignore */ }
|
|
813
|
+
}
|
|
814
|
+
return {};
|
|
815
|
+
},
|
|
816
|
+
|
|
817
|
+
// ── External purchase observation — Android parity (BillDog.kt:1864–1878) ──
|
|
818
|
+
|
|
819
|
+
/** Notify BillDog that an externally-driven purchase has started. */
|
|
820
|
+
observePurchaseStart(productId: string): void {
|
|
821
|
+
if (native?.observePurchaseStart) {
|
|
822
|
+
try { native.observePurchaseStart(productId); } catch { /* ignore */ }
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
|
|
826
|
+
/** Notify BillDog that an externally-driven purchase succeeded. */
|
|
827
|
+
observePurchaseResult(productId: string, orderId?: string): void {
|
|
828
|
+
if (native?.observePurchaseResult) {
|
|
829
|
+
try { native.observePurchaseResult(productId, orderId ?? null); } catch { /* ignore */ }
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
/** Notify BillDog that an externally-driven purchase failed. */
|
|
834
|
+
observePurchaseError(productId: string, error: string): void {
|
|
835
|
+
if (native?.observePurchaseError) {
|
|
836
|
+
try { native.observePurchaseError(productId, error); } catch { /* ignore */ }
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
|
|
840
|
+
// ── BillDogDelegate — Android parity (BillDog.kt:71) ──────────────
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Set the active delegate. Pass `null` to detach. All delegate callbacks
|
|
844
|
+
* are optional; the SDK fires the ones that are defined.
|
|
845
|
+
*/
|
|
846
|
+
setDelegate(delegate: BillDogDelegate | null): void {
|
|
847
|
+
activeDelegate = delegate;
|
|
848
|
+
},
|
|
849
|
+
|
|
850
|
+
/** Internal — emit a typed lifecycle event to the delegate (if any). */
|
|
851
|
+
_emitDelegate<K extends keyof BillDogDelegate>(
|
|
852
|
+
event: K,
|
|
853
|
+
...args: Parameters<NonNullable<BillDogDelegate[K]>>
|
|
854
|
+
): void {
|
|
855
|
+
const fn = activeDelegate?.[event] as ((...a: any[]) => void) | undefined;
|
|
856
|
+
if (typeof fn === 'function') {
|
|
857
|
+
try {
|
|
858
|
+
fn(...args);
|
|
859
|
+
} catch {
|
|
860
|
+
/* delegate errors must not propagate into SDK flow */
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Evaluate what would happen if `register(placementId)` were called, without
|
|
867
|
+
* presenting a paywall. Returns one of `paywall`, `userSubscribed`,
|
|
868
|
+
* `noPaywall`, `holdout`, or `error`.
|
|
869
|
+
*/
|
|
870
|
+
async getPresentationResult(placementId: string): Promise<PresentationResult> {
|
|
871
|
+
if (!BillDogConfiguration.isConfigured()) {
|
|
872
|
+
return { type: 'error', message: 'BillDog SDK not configured.' };
|
|
873
|
+
}
|
|
874
|
+
if (!native?.getPresentationResult) {
|
|
875
|
+
return { type: 'noPaywall', reason: 'Native placement bridge not wired' };
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
const raw = await native.getPresentationResult(placementId);
|
|
879
|
+
if (raw && typeof raw.type === 'string') return raw as PresentationResult;
|
|
880
|
+
return { type: 'error', message: 'Malformed presentation result' };
|
|
881
|
+
} catch (e: any) {
|
|
882
|
+
return { type: 'error', message: e?.message ?? 'Presentation evaluation failed' };
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Execute a server-side paywall tap action (Route B).
|
|
888
|
+
* Delegates to native SDK bridge `executeAction` if present; falls back to direct
|
|
889
|
+
* JavaScript fetch request to `execute-paywall-action` Edge Function if native is absent.
|
|
890
|
+
*/
|
|
891
|
+
async executeAction(
|
|
892
|
+
paywallId: string | null,
|
|
893
|
+
componentId: string,
|
|
894
|
+
actionType: string,
|
|
895
|
+
idempotencyKey: string | null = null,
|
|
896
|
+
metadata?: Record<string, any>
|
|
897
|
+
): Promise<any> {
|
|
898
|
+
if (native?.executeAction) {
|
|
899
|
+
try {
|
|
900
|
+
return await native.executeAction(paywallId, componentId, actionType, idempotencyKey, metadata);
|
|
901
|
+
} catch (e: any) {
|
|
902
|
+
throw e;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
const endpoint = BillDogConfiguration.resolveEndpoint('execute-paywall-action');
|
|
908
|
+
const apiKey = BillDogConfiguration.getApiKey();
|
|
909
|
+
const appUserId = this.getAppUserId();
|
|
910
|
+
|
|
911
|
+
const body: Record<string, any> = {
|
|
912
|
+
paywall_id: paywallId,
|
|
913
|
+
component_id: componentId,
|
|
914
|
+
action_type: actionType,
|
|
915
|
+
idempotency_key: idempotencyKey,
|
|
916
|
+
};
|
|
917
|
+
if (appUserId) {
|
|
918
|
+
body.app_user_id = appUserId;
|
|
919
|
+
}
|
|
920
|
+
if (metadata) {
|
|
921
|
+
body.metadata = metadata;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const response = await fetch(endpoint, {
|
|
925
|
+
method: 'POST',
|
|
926
|
+
headers: {
|
|
927
|
+
'Content-Type': 'application/json',
|
|
928
|
+
'X-BillDog-API-Key': apiKey,
|
|
929
|
+
},
|
|
930
|
+
body: JSON.stringify(body),
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
if (!response.ok) {
|
|
934
|
+
const errorText = await response.text();
|
|
935
|
+
throw new Error(errorText || `HTTP ${response.status}`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return await response.json();
|
|
939
|
+
} catch (e: any) {
|
|
940
|
+
if (BillDogConfiguration.config.enableLogging) {
|
|
941
|
+
console.error('[BillDog] Failed to execute action:', e);
|
|
942
|
+
}
|
|
943
|
+
throw e;
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Forward a single provider attribution id to the native `setAttribution`
|
|
950
|
+
* bridge. `null` is a documented no-op (native has no clear). Best-effort —
|
|
951
|
+
* native/bridge errors are swallowed so attribution never breaks a host app.
|
|
952
|
+
*/
|
|
953
|
+
function setAttributionKey(key: string, id: string | null): void {
|
|
954
|
+
if (!native?.setAttribution || id == null) return;
|
|
955
|
+
try {
|
|
956
|
+
native.setAttribution({ [key]: id });
|
|
957
|
+
} catch {
|
|
958
|
+
/* swallow — attribution is best-effort */
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Re-export the PurchaseParams namespace + builder for ergonomic imports.
|
|
963
|
+
export { PurchaseParamsNS as PurchaseParams };
|
|
964
|
+
|
|
965
|
+
export type { RestoreResult } from './models/RestoreResult';
|
|
966
|
+
export type { RestoredItem, RestoreItemStatus } from './models/RestoredItem';
|
|
967
|
+
export type { BillDogError } from './models/BillDogError';
|
|
968
|
+
export type { CacheFetchPolicy } from './models/CacheFetchPolicy';
|
|
969
|
+
export type { CustomerInfo } from './models/CustomerInfo';
|
|
970
|
+
export type { EntitlementInfo, EntitlementState } from './models/EntitlementInfo';
|
|
971
|
+
export type { PurchaseParams as PurchaseParamsType, GoogleReplacementMode } from './models/PurchaseParams';
|
|
972
|
+
export type { PurchaseResult } from './models/PurchaseResult';
|
|
973
|
+
export type { CustomProduct } from './models/CustomProduct';
|
|
974
|
+
export type {
|
|
975
|
+
PlacementResult,
|
|
976
|
+
PresentationResult,
|
|
977
|
+
PaywallLoadingState,
|
|
978
|
+
SkipReason,
|
|
979
|
+
PostPurchaseBehavior,
|
|
980
|
+
PaywallPresentationHandler,
|
|
981
|
+
} from './models/Placement';
|
|
982
|
+
export type { BillDogDelegate } from './models/Delegate';
|
|
983
|
+
|