@billdog.io/react-native 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/BillDogReactNative.podspec +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/android/build.gradle +99 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/kotlin/io/billdog/reactnative/BillDogABTestModule.kt +317 -0
  7. package/android/src/main/kotlin/io/billdog/reactnative/BillDogAnalyticsModule.kt +389 -0
  8. package/android/src/main/kotlin/io/billdog/reactnative/BillDogModule.kt +817 -0
  9. package/android/src/main/kotlin/io/billdog/reactnative/BillDogPaywallViewManager.kt +383 -0
  10. package/android/src/main/kotlin/io/billdog/reactnative/BillDogSurveyModule.kt +427 -0
  11. package/android/src/main/kotlin/io/billdog/reactnative/BillDogVirtualCurrencyModule.kt +407 -0
  12. package/android/src/main/kotlin/io/billdog/virtualcurrency/BillDogVirtualCurrencyPackage.kt +44 -0
  13. package/dist/AdvancedPaywallCache.d.ts +70 -0
  14. package/dist/AdvancedPaywallCache.js +344 -0
  15. package/dist/BillDog.d.ts +255 -0
  16. package/dist/BillDog.js +918 -0
  17. package/dist/BillDogConfiguration.d.ts +145 -0
  18. package/dist/BillDogConfiguration.js +229 -0
  19. package/dist/ComponentRenderer.d.ts +17 -0
  20. package/dist/ComponentRenderer.js +886 -0
  21. package/dist/LayoutEngine.d.ts +101 -0
  22. package/dist/LayoutEngine.js +394 -0
  23. package/dist/LocalizationResolver.d.ts +99 -0
  24. package/dist/LocalizationResolver.js +266 -0
  25. package/dist/NativePaywallView.d.ts +36 -0
  26. package/dist/NativePaywallView.js +59 -0
  27. package/dist/PaywallCache.d.ts +74 -0
  28. package/dist/PaywallCache.js +293 -0
  29. package/dist/PaywallStateManager.d.ts +62 -0
  30. package/dist/PaywallStateManager.js +147 -0
  31. package/dist/PaywallView.d.ts +42 -0
  32. package/dist/PaywallView.js +275 -0
  33. package/dist/ProductVariableProvider.d.ts +60 -0
  34. package/dist/ProductVariableProvider.js +239 -0
  35. package/dist/VariableResolver.d.ts +120 -0
  36. package/dist/VariableResolver.js +379 -0
  37. package/dist/WebViewPaywall.d.ts +75 -0
  38. package/dist/WebViewPaywall.js +360 -0
  39. package/dist/animations/presets.d.ts +49 -0
  40. package/dist/animations/presets.js +367 -0
  41. package/dist/components/AccoladeCpsComponent.d.ts +13 -0
  42. package/dist/components/AccoladeCpsComponent.js +248 -0
  43. package/dist/components/AccordionComponent.d.ts +10 -0
  44. package/dist/components/AccordionComponent.js +187 -0
  45. package/dist/components/AnimatedWrapper.d.ts +35 -0
  46. package/dist/components/AnimatedWrapper.js +277 -0
  47. package/dist/components/CarouselComponent.d.ts +23 -0
  48. package/dist/components/CarouselComponent.js +484 -0
  49. package/dist/components/ChartCurveComponent.d.ts +13 -0
  50. package/dist/components/ChartCurveComponent.js +333 -0
  51. package/dist/components/CheckboxComponent.d.ts +9 -0
  52. package/dist/components/CheckboxComponent.js +172 -0
  53. package/dist/components/ConfettiComponent.d.ts +9 -0
  54. package/dist/components/ConfettiComponent.js +172 -0
  55. package/dist/components/CountdownComponent.d.ts +16 -0
  56. package/dist/components/CountdownComponent.js +414 -0
  57. package/dist/components/DelayedXComponent.d.ts +12 -0
  58. package/dist/components/DelayedXComponent.js +240 -0
  59. package/dist/components/DrawerModalComponent.d.ts +20 -0
  60. package/dist/components/DrawerModalComponent.js +286 -0
  61. package/dist/components/GradientView.d.ts +38 -0
  62. package/dist/components/GradientView.js +126 -0
  63. package/dist/components/HapticFeedbackComponent.d.ts +16 -0
  64. package/dist/components/HapticFeedbackComponent.js +64 -0
  65. package/dist/components/HorizontalProductsComponent.d.ts +17 -0
  66. package/dist/components/HorizontalProductsComponent.js +284 -0
  67. package/dist/components/MultiChoiceGridComponent.d.ts +13 -0
  68. package/dist/components/MultiChoiceGridComponent.js +234 -0
  69. package/dist/components/NPSScaleComponent.d.ts +16 -0
  70. package/dist/components/NPSScaleComponent.js +260 -0
  71. package/dist/components/NavigationComponent.d.ts +26 -0
  72. package/dist/components/NavigationComponent.js +302 -0
  73. package/dist/components/OAuthButtonComponent.d.ts +20 -0
  74. package/dist/components/OAuthButtonComponent.js +237 -0
  75. package/dist/components/ProgressLoaderComponent.d.ts +13 -0
  76. package/dist/components/ProgressLoaderComponent.js +308 -0
  77. package/dist/components/ProgressStepperComponent.d.ts +18 -0
  78. package/dist/components/ProgressStepperComponent.js +228 -0
  79. package/dist/components/SegmentedPickerComponent.d.ts +10 -0
  80. package/dist/components/SegmentedPickerComponent.js +221 -0
  81. package/dist/components/ShareButtonsComponent.d.ts +21 -0
  82. package/dist/components/ShareButtonsComponent.js +265 -0
  83. package/dist/components/ShareCodeComponent.d.ts +16 -0
  84. package/dist/components/ShareCodeComponent.js +274 -0
  85. package/dist/components/SingleChoiceGridComponent.d.ts +13 -0
  86. package/dist/components/SingleChoiceGridComponent.js +210 -0
  87. package/dist/components/SliderComponent.d.ts +9 -0
  88. package/dist/components/SliderComponent.js +263 -0
  89. package/dist/components/StarRatingInputComponent.d.ts +20 -0
  90. package/dist/components/StarRatingInputComponent.js +198 -0
  91. package/dist/components/StickerCompositeComponent.d.ts +13 -0
  92. package/dist/components/StickerCompositeComponent.js +136 -0
  93. package/dist/components/TabsComponent.d.ts +19 -0
  94. package/dist/components/TabsComponent.js +431 -0
  95. package/dist/components/TextInputComponent.d.ts +18 -0
  96. package/dist/components/TextInputComponent.js +227 -0
  97. package/dist/components/TimelineComponent.d.ts +19 -0
  98. package/dist/components/TimelineComponent.js +104 -0
  99. package/dist/components/ToggleComponent.d.ts +14 -0
  100. package/dist/components/ToggleComponent.js +132 -0
  101. package/dist/components/VerticalProductsComponent.d.ts +17 -0
  102. package/dist/components/VerticalProductsComponent.js +311 -0
  103. package/dist/dynamicValues.d.ts +36 -0
  104. package/dist/dynamicValues.js +142 -0
  105. package/dist/filters/dateFilters.d.ts +50 -0
  106. package/dist/filters/dateFilters.js +390 -0
  107. package/dist/filters/index.d.ts +18 -0
  108. package/dist/filters/index.js +253 -0
  109. package/dist/hooks/useOfferings.d.ts +76 -0
  110. package/dist/hooks/useOfferings.js +99 -0
  111. package/dist/hooks/usePaywall.d.ts +27 -0
  112. package/dist/hooks/usePaywall.js +114 -0
  113. package/dist/hooks/usePlacement.d.ts +34 -0
  114. package/dist/hooks/usePlacement.js +89 -0
  115. package/dist/hooks/useSubscription.d.ts +40 -0
  116. package/dist/hooks/useSubscription.js +97 -0
  117. package/dist/index.d.ts +98 -0
  118. package/dist/index.js +198 -0
  119. package/dist/models/BillDogError.d.ts +40 -0
  120. package/dist/models/BillDogError.js +11 -0
  121. package/dist/models/CacheFetchPolicy.d.ts +8 -0
  122. package/dist/models/CacheFetchPolicy.js +4 -0
  123. package/dist/models/CustomProduct.d.ts +32 -0
  124. package/dist/models/CustomProduct.js +15 -0
  125. package/dist/models/CustomerInfo.d.ts +17 -0
  126. package/dist/models/CustomerInfo.js +11 -0
  127. package/dist/models/Delegate.d.ts +20 -0
  128. package/dist/models/Delegate.js +11 -0
  129. package/dist/models/EntitlementInfo.d.ts +19 -0
  130. package/dist/models/EntitlementInfo.js +2 -0
  131. package/dist/models/Placement.d.ts +54 -0
  132. package/dist/models/Placement.js +15 -0
  133. package/dist/models/PurchaseParams.d.ts +39 -0
  134. package/dist/models/PurchaseParams.js +60 -0
  135. package/dist/models/PurchaseResult.d.ts +30 -0
  136. package/dist/models/PurchaseResult.js +15 -0
  137. package/dist/models/RestoreResult.d.ts +30 -0
  138. package/dist/models/RestoreResult.js +22 -0
  139. package/dist/models/RestoredItem.d.ts +26 -0
  140. package/dist/models/RestoredItem.js +13 -0
  141. package/dist/modules/ABTest.d.ts +65 -0
  142. package/dist/modules/ABTest.js +113 -0
  143. package/dist/modules/Analytics.d.ts +91 -0
  144. package/dist/modules/Analytics.js +182 -0
  145. package/dist/modules/Survey.d.ts +65 -0
  146. package/dist/modules/Survey.js +82 -0
  147. package/dist/modules/VirtualCurrency.d.ts +63 -0
  148. package/dist/modules/VirtualCurrency.js +147 -0
  149. package/dist/murmur.d.ts +23 -0
  150. package/dist/murmur.js +104 -0
  151. package/dist/notifications/LiveActivitiesManager.d.ts +47 -0
  152. package/dist/notifications/LiveActivitiesManager.js +106 -0
  153. package/dist/notifications/NotificationScheduler.d.ts +55 -0
  154. package/dist/notifications/NotificationScheduler.js +199 -0
  155. package/dist/notifications/PurchaseNotificationHandler.d.ts +24 -0
  156. package/dist/notifications/PurchaseNotificationHandler.js +55 -0
  157. package/dist/notifications/useDeviceToken.d.ts +36 -0
  158. package/dist/notifications/useDeviceToken.js +97 -0
  159. package/dist/notifications/useLiveActivity.d.ts +39 -0
  160. package/dist/notifications/useLiveActivity.js +104 -0
  161. package/dist/review/ReviewAnalytics.d.ts +34 -0
  162. package/dist/review/ReviewAnalytics.js +73 -0
  163. package/dist/review/ReviewManager.d.ts +83 -0
  164. package/dist/review/ReviewManager.js +339 -0
  165. package/dist/review/ReviewTimingManager.d.ts +82 -0
  166. package/dist/review/ReviewTimingManager.js +224 -0
  167. package/dist/survey-core/types.d.ts +215 -0
  168. package/dist/survey-core/types.js +8 -0
  169. package/dist/types.d.ts +438 -0
  170. package/dist/types.js +158 -0
  171. package/dist/utils/ApiClient.d.ts +65 -0
  172. package/dist/utils/ApiClient.js +132 -0
  173. package/dist/utils/CSSConverter.d.ts +49 -0
  174. package/dist/utils/CSSConverter.js +470 -0
  175. package/dist/utils/animations.d.ts +41 -0
  176. package/dist/utils/animations.js +194 -0
  177. package/dist/utils/calendarIntegration.d.ts +36 -0
  178. package/dist/utils/calendarIntegration.js +89 -0
  179. package/dist/utils/configurationResolver.d.ts +23 -0
  180. package/dist/utils/configurationResolver.js +78 -0
  181. package/dist/utils/deviceDetection.d.ts +38 -0
  182. package/dist/utils/deviceDetection.js +59 -0
  183. package/dist/utils/hapticFeedback.d.ts +17 -0
  184. package/dist/utils/hapticFeedback.js +53 -0
  185. package/dist/utils/iconMapper.d.ts +26 -0
  186. package/dist/utils/iconMapper.js +113 -0
  187. package/dist/utils/notificationPermissions.d.ts +13 -0
  188. package/dist/utils/notificationPermissions.js +51 -0
  189. package/dist/utils/priceFormatting.d.ts +19 -0
  190. package/dist/utils/priceFormatting.js +99 -0
  191. package/dist/utils/shareSheet.d.ts +17 -0
  192. package/dist/utils/shareSheet.js +42 -0
  193. package/dist/utils/subscriptionUpgrade.d.ts +84 -0
  194. package/dist/utils/subscriptionUpgrade.js +100 -0
  195. package/dist/valueUtils.d.ts +48 -0
  196. package/dist/valueUtils.js +129 -0
  197. package/dist/xFlowTags.d.ts +18 -0
  198. package/dist/xFlowTags.js +385 -0
  199. package/ios/BillDogABTest.m +50 -0
  200. package/ios/BillDogABTest.swift +188 -0
  201. package/ios/BillDogAnalytics.m +105 -0
  202. package/ios/BillDogAnalytics.swift +324 -0
  203. package/ios/BillDogBridge.m +222 -0
  204. package/ios/BillDogBridge.swift +715 -0
  205. package/ios/BillDogLiveActivities.m +41 -0
  206. package/ios/BillDogLiveActivities.swift +104 -0
  207. package/ios/BillDogPaywallViewManager.m +39 -0
  208. package/ios/BillDogPaywallViewManager.swift +361 -0
  209. package/ios/BillDogSurvey.m +34 -0
  210. package/ios/BillDogSurvey.swift +257 -0
  211. package/ios/BillDogVirtualCurrencyBridge.m +63 -0
  212. package/ios/BillDogVirtualCurrencyBridge.swift +511 -0
  213. package/junit.xml +25 -0
  214. package/package.json +61 -0
  215. package/react-native.config.js +13 -0
  216. package/src/AdvancedPaywallCache.ts +433 -0
  217. package/src/BillDog.ts +983 -0
  218. package/src/BillDogConfiguration.ts +318 -0
  219. package/src/ComponentRenderer.tsx +1325 -0
  220. package/src/LayoutEngine.ts +459 -0
  221. package/src/LocalizationResolver.ts +311 -0
  222. package/src/NativePaywallView.tsx +132 -0
  223. package/src/PaywallCache.ts +358 -0
  224. package/src/PaywallStateManager.ts +154 -0
  225. package/src/PaywallView.tsx +328 -0
  226. package/src/ProductVariableProvider.ts +282 -0
  227. package/src/VariableResolver.ts +516 -0
  228. package/src/WebViewPaywall.tsx +454 -0
  229. package/src/animations/presets.ts +421 -0
  230. package/src/components/AccoladeCpsComponent.tsx +259 -0
  231. package/src/components/AccordionComponent.tsx +213 -0
  232. package/src/components/AnimatedWrapper.tsx +316 -0
  233. package/src/components/CarouselComponent.tsx +615 -0
  234. package/src/components/ChartCurveComponent.tsx +452 -0
  235. package/src/components/CheckboxComponent.tsx +185 -0
  236. package/src/components/ConfettiComponent.tsx +178 -0
  237. package/src/components/CountdownComponent.tsx +454 -0
  238. package/src/components/DelayedXComponent.tsx +266 -0
  239. package/src/components/DrawerModalComponent.tsx +324 -0
  240. package/src/components/GradientView.tsx +129 -0
  241. package/src/components/HapticFeedbackComponent.tsx +39 -0
  242. package/src/components/HorizontalProductsComponent.tsx +329 -0
  243. package/src/components/MultiChoiceGridComponent.tsx +265 -0
  244. package/src/components/NPSScaleComponent.tsx +294 -0
  245. package/src/components/NavigationComponent.tsx +329 -0
  246. package/src/components/OAuthButtonComponent.tsx +266 -0
  247. package/src/components/ProgressLoaderComponent.tsx +366 -0
  248. package/src/components/ProgressStepperComponent.tsx +250 -0
  249. package/src/components/SegmentedPickerComponent.tsx +245 -0
  250. package/src/components/ShareButtonsComponent.tsx +299 -0
  251. package/src/components/ShareCodeComponent.tsx +303 -0
  252. package/src/components/SingleChoiceGridComponent.tsx +229 -0
  253. package/src/components/SliderComponent.tsx +280 -0
  254. package/src/components/StarRatingInputComponent.tsx +223 -0
  255. package/src/components/StickerCompositeComponent.tsx +168 -0
  256. package/src/components/TabsComponent.tsx +496 -0
  257. package/src/components/TextInputComponent.tsx +258 -0
  258. package/src/components/TimelineComponent.tsx +129 -0
  259. package/src/components/ToggleComponent.tsx +142 -0
  260. package/src/components/VerticalProductsComponent.tsx +357 -0
  261. package/src/dynamicValues.ts +180 -0
  262. package/src/filters/dateFilters.ts +411 -0
  263. package/src/filters/index.ts +281 -0
  264. package/src/hooks/useOfferings.ts +192 -0
  265. package/src/hooks/usePaywall.ts +154 -0
  266. package/src/hooks/usePlacement.ts +143 -0
  267. package/src/hooks/useSubscription.ts +154 -0
  268. package/src/index.ts +245 -0
  269. package/src/models/BillDogError.ts +24 -0
  270. package/src/models/CacheFetchPolicy.ts +13 -0
  271. package/src/models/CustomProduct.ts +33 -0
  272. package/src/models/CustomerInfo.ts +26 -0
  273. package/src/models/Delegate.ts +22 -0
  274. package/src/models/EntitlementInfo.ts +27 -0
  275. package/src/models/Placement.ts +50 -0
  276. package/src/models/PurchaseParams.ts +81 -0
  277. package/src/models/PurchaseResult.ts +32 -0
  278. package/src/models/RestoreResult.ts +27 -0
  279. package/src/models/RestoredItem.ts +25 -0
  280. package/src/modules/ABTest.ts +131 -0
  281. package/src/modules/Analytics.ts +227 -0
  282. package/src/modules/Survey.ts +126 -0
  283. package/src/modules/VirtualCurrency.ts +223 -0
  284. package/src/murmur.ts +102 -0
  285. package/src/notifications/LiveActivitiesManager.ts +140 -0
  286. package/src/notifications/NotificationScheduler.ts +292 -0
  287. package/src/notifications/PurchaseNotificationHandler.ts +67 -0
  288. package/src/notifications/useDeviceToken.ts +116 -0
  289. package/src/notifications/useLiveActivity.ts +125 -0
  290. package/src/review/ReviewAnalytics.ts +89 -0
  291. package/src/review/ReviewManager.ts +355 -0
  292. package/src/review/ReviewTimingManager.ts +265 -0
  293. package/src/survey-core/types.ts +244 -0
  294. package/src/types/ambient.d.ts +9 -0
  295. package/src/types.ts +680 -0
  296. package/src/utils/ApiClient.ts +163 -0
  297. package/src/utils/CSSConverter.ts +552 -0
  298. package/src/utils/animations.ts +232 -0
  299. package/src/utils/calendarIntegration.ts +113 -0
  300. package/src/utils/configurationResolver.ts +106 -0
  301. package/src/utils/deviceDetection.ts +83 -0
  302. package/src/utils/hapticFeedback.ts +55 -0
  303. package/src/utils/iconMapper.ts +102 -0
  304. package/src/utils/notificationPermissions.ts +48 -0
  305. package/src/utils/priceFormatting.ts +119 -0
  306. package/src/utils/shareSheet.ts +55 -0
  307. package/src/utils/subscriptionUpgrade.ts +183 -0
  308. package/src/valueUtils.ts +108 -0
  309. package/src/xFlowTags.ts +463 -0
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
+