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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/BillDogReactNative.podspec +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/android/build.gradle +99 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/kotlin/io/billdog/reactnative/BillDogABTestModule.kt +317 -0
  7. package/android/src/main/kotlin/io/billdog/reactnative/BillDogAnalyticsModule.kt +389 -0
  8. package/android/src/main/kotlin/io/billdog/reactnative/BillDogModule.kt +817 -0
  9. package/android/src/main/kotlin/io/billdog/reactnative/BillDogPaywallViewManager.kt +383 -0
  10. package/android/src/main/kotlin/io/billdog/reactnative/BillDogSurveyModule.kt +427 -0
  11. package/android/src/main/kotlin/io/billdog/reactnative/BillDogVirtualCurrencyModule.kt +407 -0
  12. package/android/src/main/kotlin/io/billdog/virtualcurrency/BillDogVirtualCurrencyPackage.kt +44 -0
  13. package/dist/AdvancedPaywallCache.d.ts +70 -0
  14. package/dist/AdvancedPaywallCache.js +344 -0
  15. package/dist/BillDog.d.ts +255 -0
  16. package/dist/BillDog.js +918 -0
  17. package/dist/BillDogConfiguration.d.ts +145 -0
  18. package/dist/BillDogConfiguration.js +229 -0
  19. package/dist/ComponentRenderer.d.ts +17 -0
  20. package/dist/ComponentRenderer.js +886 -0
  21. package/dist/LayoutEngine.d.ts +101 -0
  22. package/dist/LayoutEngine.js +394 -0
  23. package/dist/LocalizationResolver.d.ts +99 -0
  24. package/dist/LocalizationResolver.js +266 -0
  25. package/dist/NativePaywallView.d.ts +36 -0
  26. package/dist/NativePaywallView.js +59 -0
  27. package/dist/PaywallCache.d.ts +74 -0
  28. package/dist/PaywallCache.js +293 -0
  29. package/dist/PaywallStateManager.d.ts +62 -0
  30. package/dist/PaywallStateManager.js +147 -0
  31. package/dist/PaywallView.d.ts +42 -0
  32. package/dist/PaywallView.js +275 -0
  33. package/dist/ProductVariableProvider.d.ts +60 -0
  34. package/dist/ProductVariableProvider.js +239 -0
  35. package/dist/VariableResolver.d.ts +120 -0
  36. package/dist/VariableResolver.js +379 -0
  37. package/dist/WebViewPaywall.d.ts +75 -0
  38. package/dist/WebViewPaywall.js +360 -0
  39. package/dist/animations/presets.d.ts +49 -0
  40. package/dist/animations/presets.js +367 -0
  41. package/dist/components/AccoladeCpsComponent.d.ts +13 -0
  42. package/dist/components/AccoladeCpsComponent.js +248 -0
  43. package/dist/components/AccordionComponent.d.ts +10 -0
  44. package/dist/components/AccordionComponent.js +187 -0
  45. package/dist/components/AnimatedWrapper.d.ts +35 -0
  46. package/dist/components/AnimatedWrapper.js +277 -0
  47. package/dist/components/CarouselComponent.d.ts +23 -0
  48. package/dist/components/CarouselComponent.js +484 -0
  49. package/dist/components/ChartCurveComponent.d.ts +13 -0
  50. package/dist/components/ChartCurveComponent.js +333 -0
  51. package/dist/components/CheckboxComponent.d.ts +9 -0
  52. package/dist/components/CheckboxComponent.js +172 -0
  53. package/dist/components/ConfettiComponent.d.ts +9 -0
  54. package/dist/components/ConfettiComponent.js +172 -0
  55. package/dist/components/CountdownComponent.d.ts +16 -0
  56. package/dist/components/CountdownComponent.js +414 -0
  57. package/dist/components/DelayedXComponent.d.ts +12 -0
  58. package/dist/components/DelayedXComponent.js +240 -0
  59. package/dist/components/DrawerModalComponent.d.ts +20 -0
  60. package/dist/components/DrawerModalComponent.js +286 -0
  61. package/dist/components/GradientView.d.ts +38 -0
  62. package/dist/components/GradientView.js +126 -0
  63. package/dist/components/HapticFeedbackComponent.d.ts +16 -0
  64. package/dist/components/HapticFeedbackComponent.js +64 -0
  65. package/dist/components/HorizontalProductsComponent.d.ts +17 -0
  66. package/dist/components/HorizontalProductsComponent.js +284 -0
  67. package/dist/components/MultiChoiceGridComponent.d.ts +13 -0
  68. package/dist/components/MultiChoiceGridComponent.js +234 -0
  69. package/dist/components/NPSScaleComponent.d.ts +16 -0
  70. package/dist/components/NPSScaleComponent.js +260 -0
  71. package/dist/components/NavigationComponent.d.ts +26 -0
  72. package/dist/components/NavigationComponent.js +302 -0
  73. package/dist/components/OAuthButtonComponent.d.ts +20 -0
  74. package/dist/components/OAuthButtonComponent.js +237 -0
  75. package/dist/components/ProgressLoaderComponent.d.ts +13 -0
  76. package/dist/components/ProgressLoaderComponent.js +308 -0
  77. package/dist/components/ProgressStepperComponent.d.ts +18 -0
  78. package/dist/components/ProgressStepperComponent.js +228 -0
  79. package/dist/components/SegmentedPickerComponent.d.ts +10 -0
  80. package/dist/components/SegmentedPickerComponent.js +221 -0
  81. package/dist/components/ShareButtonsComponent.d.ts +21 -0
  82. package/dist/components/ShareButtonsComponent.js +265 -0
  83. package/dist/components/ShareCodeComponent.d.ts +16 -0
  84. package/dist/components/ShareCodeComponent.js +274 -0
  85. package/dist/components/SingleChoiceGridComponent.d.ts +13 -0
  86. package/dist/components/SingleChoiceGridComponent.js +210 -0
  87. package/dist/components/SliderComponent.d.ts +9 -0
  88. package/dist/components/SliderComponent.js +263 -0
  89. package/dist/components/StarRatingInputComponent.d.ts +20 -0
  90. package/dist/components/StarRatingInputComponent.js +198 -0
  91. package/dist/components/StickerCompositeComponent.d.ts +13 -0
  92. package/dist/components/StickerCompositeComponent.js +136 -0
  93. package/dist/components/TabsComponent.d.ts +19 -0
  94. package/dist/components/TabsComponent.js +431 -0
  95. package/dist/components/TextInputComponent.d.ts +18 -0
  96. package/dist/components/TextInputComponent.js +227 -0
  97. package/dist/components/TimelineComponent.d.ts +19 -0
  98. package/dist/components/TimelineComponent.js +104 -0
  99. package/dist/components/ToggleComponent.d.ts +14 -0
  100. package/dist/components/ToggleComponent.js +132 -0
  101. package/dist/components/VerticalProductsComponent.d.ts +17 -0
  102. package/dist/components/VerticalProductsComponent.js +311 -0
  103. package/dist/dynamicValues.d.ts +36 -0
  104. package/dist/dynamicValues.js +142 -0
  105. package/dist/filters/dateFilters.d.ts +50 -0
  106. package/dist/filters/dateFilters.js +390 -0
  107. package/dist/filters/index.d.ts +18 -0
  108. package/dist/filters/index.js +253 -0
  109. package/dist/hooks/useOfferings.d.ts +76 -0
  110. package/dist/hooks/useOfferings.js +99 -0
  111. package/dist/hooks/usePaywall.d.ts +27 -0
  112. package/dist/hooks/usePaywall.js +114 -0
  113. package/dist/hooks/usePlacement.d.ts +34 -0
  114. package/dist/hooks/usePlacement.js +89 -0
  115. package/dist/hooks/useSubscription.d.ts +40 -0
  116. package/dist/hooks/useSubscription.js +97 -0
  117. package/dist/index.d.ts +98 -0
  118. package/dist/index.js +198 -0
  119. package/dist/models/BillDogError.d.ts +40 -0
  120. package/dist/models/BillDogError.js +11 -0
  121. package/dist/models/CacheFetchPolicy.d.ts +8 -0
  122. package/dist/models/CacheFetchPolicy.js +4 -0
  123. package/dist/models/CustomProduct.d.ts +32 -0
  124. package/dist/models/CustomProduct.js +15 -0
  125. package/dist/models/CustomerInfo.d.ts +17 -0
  126. package/dist/models/CustomerInfo.js +11 -0
  127. package/dist/models/Delegate.d.ts +20 -0
  128. package/dist/models/Delegate.js +11 -0
  129. package/dist/models/EntitlementInfo.d.ts +19 -0
  130. package/dist/models/EntitlementInfo.js +2 -0
  131. package/dist/models/Placement.d.ts +54 -0
  132. package/dist/models/Placement.js +15 -0
  133. package/dist/models/PurchaseParams.d.ts +39 -0
  134. package/dist/models/PurchaseParams.js +60 -0
  135. package/dist/models/PurchaseResult.d.ts +30 -0
  136. package/dist/models/PurchaseResult.js +15 -0
  137. package/dist/models/RestoreResult.d.ts +30 -0
  138. package/dist/models/RestoreResult.js +22 -0
  139. package/dist/models/RestoredItem.d.ts +26 -0
  140. package/dist/models/RestoredItem.js +13 -0
  141. package/dist/modules/ABTest.d.ts +65 -0
  142. package/dist/modules/ABTest.js +113 -0
  143. package/dist/modules/Analytics.d.ts +91 -0
  144. package/dist/modules/Analytics.js +182 -0
  145. package/dist/modules/Survey.d.ts +65 -0
  146. package/dist/modules/Survey.js +82 -0
  147. package/dist/modules/VirtualCurrency.d.ts +63 -0
  148. package/dist/modules/VirtualCurrency.js +147 -0
  149. package/dist/murmur.d.ts +23 -0
  150. package/dist/murmur.js +104 -0
  151. package/dist/notifications/LiveActivitiesManager.d.ts +47 -0
  152. package/dist/notifications/LiveActivitiesManager.js +106 -0
  153. package/dist/notifications/NotificationScheduler.d.ts +55 -0
  154. package/dist/notifications/NotificationScheduler.js +199 -0
  155. package/dist/notifications/PurchaseNotificationHandler.d.ts +24 -0
  156. package/dist/notifications/PurchaseNotificationHandler.js +55 -0
  157. package/dist/notifications/useDeviceToken.d.ts +36 -0
  158. package/dist/notifications/useDeviceToken.js +97 -0
  159. package/dist/notifications/useLiveActivity.d.ts +39 -0
  160. package/dist/notifications/useLiveActivity.js +104 -0
  161. package/dist/review/ReviewAnalytics.d.ts +34 -0
  162. package/dist/review/ReviewAnalytics.js +73 -0
  163. package/dist/review/ReviewManager.d.ts +83 -0
  164. package/dist/review/ReviewManager.js +339 -0
  165. package/dist/review/ReviewTimingManager.d.ts +82 -0
  166. package/dist/review/ReviewTimingManager.js +224 -0
  167. package/dist/survey-core/types.d.ts +215 -0
  168. package/dist/survey-core/types.js +8 -0
  169. package/dist/types.d.ts +438 -0
  170. package/dist/types.js +158 -0
  171. package/dist/utils/ApiClient.d.ts +65 -0
  172. package/dist/utils/ApiClient.js +132 -0
  173. package/dist/utils/CSSConverter.d.ts +49 -0
  174. package/dist/utils/CSSConverter.js +470 -0
  175. package/dist/utils/animations.d.ts +41 -0
  176. package/dist/utils/animations.js +194 -0
  177. package/dist/utils/calendarIntegration.d.ts +36 -0
  178. package/dist/utils/calendarIntegration.js +89 -0
  179. package/dist/utils/configurationResolver.d.ts +23 -0
  180. package/dist/utils/configurationResolver.js +78 -0
  181. package/dist/utils/deviceDetection.d.ts +38 -0
  182. package/dist/utils/deviceDetection.js +59 -0
  183. package/dist/utils/hapticFeedback.d.ts +17 -0
  184. package/dist/utils/hapticFeedback.js +53 -0
  185. package/dist/utils/iconMapper.d.ts +26 -0
  186. package/dist/utils/iconMapper.js +113 -0
  187. package/dist/utils/notificationPermissions.d.ts +13 -0
  188. package/dist/utils/notificationPermissions.js +51 -0
  189. package/dist/utils/priceFormatting.d.ts +19 -0
  190. package/dist/utils/priceFormatting.js +99 -0
  191. package/dist/utils/shareSheet.d.ts +17 -0
  192. package/dist/utils/shareSheet.js +42 -0
  193. package/dist/utils/subscriptionUpgrade.d.ts +84 -0
  194. package/dist/utils/subscriptionUpgrade.js +100 -0
  195. package/dist/valueUtils.d.ts +48 -0
  196. package/dist/valueUtils.js +129 -0
  197. package/dist/xFlowTags.d.ts +18 -0
  198. package/dist/xFlowTags.js +385 -0
  199. package/ios/BillDogABTest.m +50 -0
  200. package/ios/BillDogABTest.swift +188 -0
  201. package/ios/BillDogAnalytics.m +105 -0
  202. package/ios/BillDogAnalytics.swift +324 -0
  203. package/ios/BillDogBridge.m +222 -0
  204. package/ios/BillDogBridge.swift +715 -0
  205. package/ios/BillDogLiveActivities.m +41 -0
  206. package/ios/BillDogLiveActivities.swift +104 -0
  207. package/ios/BillDogPaywallViewManager.m +39 -0
  208. package/ios/BillDogPaywallViewManager.swift +361 -0
  209. package/ios/BillDogSurvey.m +34 -0
  210. package/ios/BillDogSurvey.swift +257 -0
  211. package/ios/BillDogVirtualCurrencyBridge.m +63 -0
  212. package/ios/BillDogVirtualCurrencyBridge.swift +511 -0
  213. package/junit.xml +25 -0
  214. package/package.json +61 -0
  215. package/react-native.config.js +13 -0
  216. package/src/AdvancedPaywallCache.ts +433 -0
  217. package/src/BillDog.ts +983 -0
  218. package/src/BillDogConfiguration.ts +318 -0
  219. package/src/ComponentRenderer.tsx +1325 -0
  220. package/src/LayoutEngine.ts +459 -0
  221. package/src/LocalizationResolver.ts +311 -0
  222. package/src/NativePaywallView.tsx +132 -0
  223. package/src/PaywallCache.ts +358 -0
  224. package/src/PaywallStateManager.ts +154 -0
  225. package/src/PaywallView.tsx +328 -0
  226. package/src/ProductVariableProvider.ts +282 -0
  227. package/src/VariableResolver.ts +516 -0
  228. package/src/WebViewPaywall.tsx +454 -0
  229. package/src/animations/presets.ts +421 -0
  230. package/src/components/AccoladeCpsComponent.tsx +259 -0
  231. package/src/components/AccordionComponent.tsx +213 -0
  232. package/src/components/AnimatedWrapper.tsx +316 -0
  233. package/src/components/CarouselComponent.tsx +615 -0
  234. package/src/components/ChartCurveComponent.tsx +452 -0
  235. package/src/components/CheckboxComponent.tsx +185 -0
  236. package/src/components/ConfettiComponent.tsx +178 -0
  237. package/src/components/CountdownComponent.tsx +454 -0
  238. package/src/components/DelayedXComponent.tsx +266 -0
  239. package/src/components/DrawerModalComponent.tsx +324 -0
  240. package/src/components/GradientView.tsx +129 -0
  241. package/src/components/HapticFeedbackComponent.tsx +39 -0
  242. package/src/components/HorizontalProductsComponent.tsx +329 -0
  243. package/src/components/MultiChoiceGridComponent.tsx +265 -0
  244. package/src/components/NPSScaleComponent.tsx +294 -0
  245. package/src/components/NavigationComponent.tsx +329 -0
  246. package/src/components/OAuthButtonComponent.tsx +266 -0
  247. package/src/components/ProgressLoaderComponent.tsx +366 -0
  248. package/src/components/ProgressStepperComponent.tsx +250 -0
  249. package/src/components/SegmentedPickerComponent.tsx +245 -0
  250. package/src/components/ShareButtonsComponent.tsx +299 -0
  251. package/src/components/ShareCodeComponent.tsx +303 -0
  252. package/src/components/SingleChoiceGridComponent.tsx +229 -0
  253. package/src/components/SliderComponent.tsx +280 -0
  254. package/src/components/StarRatingInputComponent.tsx +223 -0
  255. package/src/components/StickerCompositeComponent.tsx +168 -0
  256. package/src/components/TabsComponent.tsx +496 -0
  257. package/src/components/TextInputComponent.tsx +258 -0
  258. package/src/components/TimelineComponent.tsx +129 -0
  259. package/src/components/ToggleComponent.tsx +142 -0
  260. package/src/components/VerticalProductsComponent.tsx +357 -0
  261. package/src/dynamicValues.ts +180 -0
  262. package/src/filters/dateFilters.ts +411 -0
  263. package/src/filters/index.ts +281 -0
  264. package/src/hooks/useOfferings.ts +192 -0
  265. package/src/hooks/usePaywall.ts +154 -0
  266. package/src/hooks/usePlacement.ts +143 -0
  267. package/src/hooks/useSubscription.ts +154 -0
  268. package/src/index.ts +245 -0
  269. package/src/models/BillDogError.ts +24 -0
  270. package/src/models/CacheFetchPolicy.ts +13 -0
  271. package/src/models/CustomProduct.ts +33 -0
  272. package/src/models/CustomerInfo.ts +26 -0
  273. package/src/models/Delegate.ts +22 -0
  274. package/src/models/EntitlementInfo.ts +27 -0
  275. package/src/models/Placement.ts +50 -0
  276. package/src/models/PurchaseParams.ts +81 -0
  277. package/src/models/PurchaseResult.ts +32 -0
  278. package/src/models/RestoreResult.ts +27 -0
  279. package/src/models/RestoredItem.ts +25 -0
  280. package/src/modules/ABTest.ts +131 -0
  281. package/src/modules/Analytics.ts +227 -0
  282. package/src/modules/Survey.ts +126 -0
  283. package/src/modules/VirtualCurrency.ts +223 -0
  284. package/src/murmur.ts +102 -0
  285. package/src/notifications/LiveActivitiesManager.ts +140 -0
  286. package/src/notifications/NotificationScheduler.ts +292 -0
  287. package/src/notifications/PurchaseNotificationHandler.ts +67 -0
  288. package/src/notifications/useDeviceToken.ts +116 -0
  289. package/src/notifications/useLiveActivity.ts +125 -0
  290. package/src/review/ReviewAnalytics.ts +89 -0
  291. package/src/review/ReviewManager.ts +355 -0
  292. package/src/review/ReviewTimingManager.ts +265 -0
  293. package/src/survey-core/types.ts +244 -0
  294. package/src/types/ambient.d.ts +9 -0
  295. package/src/types.ts +680 -0
  296. package/src/utils/ApiClient.ts +163 -0
  297. package/src/utils/CSSConverter.ts +552 -0
  298. package/src/utils/animations.ts +232 -0
  299. package/src/utils/calendarIntegration.ts +113 -0
  300. package/src/utils/configurationResolver.ts +106 -0
  301. package/src/utils/deviceDetection.ts +83 -0
  302. package/src/utils/hapticFeedback.ts +55 -0
  303. package/src/utils/iconMapper.ts +102 -0
  304. package/src/utils/notificationPermissions.ts +48 -0
  305. package/src/utils/priceFormatting.ts +119 -0
  306. package/src/utils/shareSheet.ts +55 -0
  307. package/src/utils/subscriptionUpgrade.ts +183 -0
  308. package/src/valueUtils.ts +108 -0
  309. package/src/xFlowTags.ts +463 -0
@@ -0,0 +1,817 @@
1
+ package io.billdog.reactnative
2
+
3
+ import android.app.Activity
4
+ import android.net.Uri
5
+ import android.util.Log
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.facebook.react.bridge.Promise
8
+ import com.facebook.react.bridge.ReactApplicationContext
9
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
10
+ import com.facebook.react.bridge.ReactMethod
11
+ import com.facebook.react.bridge.ReadableArray
12
+ import com.facebook.react.bridge.ReadableMap
13
+ import com.facebook.react.bridge.WritableArray
14
+ import com.facebook.react.bridge.WritableMap
15
+ import io.billdog.core.BillDog
16
+ import io.billdog.core.BillDogError
17
+ import io.billdog.core.InterfaceStyle
18
+ import io.billdog.core.PresentationResult
19
+ import io.billdog.core.PurchaseResult
20
+ import io.billdog.core.RestoreResult
21
+ import io.billdog.core.models.CustomerInfo
22
+ import io.billdog.core.models.EntitlementInfo
23
+ import kotlinx.coroutines.CoroutineScope
24
+ import kotlinx.coroutines.Dispatchers
25
+ import kotlinx.coroutines.SupervisorJob
26
+ import kotlinx.coroutines.cancel
27
+ import kotlinx.coroutines.launch
28
+ import java.util.concurrent.atomic.AtomicBoolean
29
+
30
+ /**
31
+ * BillDog — core React Native native module for Android (`NativeModules.BillDog`).
32
+ *
33
+ * Thin bridge: every `@ReactMethod` here delegates to the native BillDog Android
34
+ * SDK (`io.billdog.core.BillDog`, a Kotlin `object`) rather than reimplementing
35
+ * logic in JS. The authoritative JS contract is `src/BillDog.ts`; each
36
+ * `native.<method>(...)` call there maps to the identically-named method below.
37
+ *
38
+ * ⚠️ VERIFICATION: this file cannot be compiled in this environment — there is
39
+ * no Android/Gradle toolchain present. It needs a Gradle/Android build
40
+ * (`./gradlew :billdog_react-native:compileDebugKotlin` or the example app
41
+ * build) to type-check against the resolved `io.billdog:billdog-core` +
42
+ * kotlinx-coroutines artifacts declared in android/build.gradle. Every native
43
+ * symbol below is cited to its source file:line in billdog-core.
44
+ *
45
+ * Activity acquisition: unlike the Flutter plugin (ActivityAware), the current
46
+ * Activity is read from `currentActivity` (ReactContextBaseJavaModule). For the
47
+ * Activity-required calls (registerPlacement, showManageSubscriptions,
48
+ * showInAppMessages) a null currentActivity rejects with "NO_ACTIVITY".
49
+ */
50
+ class BillDogModule(reactContext: ReactApplicationContext) :
51
+ ReactContextBaseJavaModule(reactContext) {
52
+
53
+ private val TAG = "BillDog"
54
+
55
+ /** Suspend calls are launched here; cancelled in invalidate()/onCatalystInstanceDestroy(). */
56
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
57
+
58
+ /** Guards registerOptionalModules() — `BillDog.use()` does not de-dupe. */
59
+ private val modulesRegistered = AtomicBoolean(false)
60
+
61
+ /**
62
+ * PUR-364: JS-side PurchaseController active flag. Full JS-PurchaseController
63
+ * bridging (routing native purchase() through a JS-driven EXTERNAL flow) is
64
+ * DEFERRED — for now we only remember whether the JS facade has a host
65
+ * controller registered. When true, the JS facade drives the transaction
66
+ * itself and never calls native.purchase(), so this is purely informational.
67
+ */
68
+ @Volatile
69
+ private var purchaseControllerActive: Boolean = false
70
+
71
+ override fun getName(): String = "BillDog"
72
+
73
+ // ── Lifecycle ──────────────────────────────────────────────────────────
74
+
75
+ override fun invalidate() {
76
+ scope.cancel()
77
+ super.invalidate()
78
+ }
79
+
80
+ @Deprecated("Deprecated in RN; kept for older RN versions that call it instead of invalidate()")
81
+ override fun onCatalystInstanceDestroy() {
82
+ scope.cancel()
83
+ @Suppress("DEPRECATION")
84
+ super.onCatalystInstanceDestroy()
85
+ }
86
+
87
+ // ── Coroutine helpers ──────────────────────────────────────────────────
88
+ // Launched on Dispatchers.Main so the suspend body resumes on the main
89
+ // thread and promise.resolve/reject are invoked there.
90
+
91
+ /** Run a suspend [block] that produces a resolve value; map any throwable → promise.reject. */
92
+ private fun launch(promise: Promise, block: suspend () -> Any?) {
93
+ scope.launch {
94
+ try {
95
+ promise.resolve(block())
96
+ } catch (e: Throwable) {
97
+ promise.reject("BILLDOG_ERROR", e.message, e)
98
+ }
99
+ }
100
+ }
101
+
102
+ // ── Configuration ──────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * native.configure(apiKey, options) → BillDog.configure(context, apiKey, config)
106
+ * (BillDog.kt:263). This is what makes the native SDK usable — without it the
107
+ * native singleton is unconfigured and every delegating call fails. The RN
108
+ * `Context` comes from `reactContext.applicationContext`.
109
+ */
110
+ @ReactMethod
111
+ fun configure(apiKey: String, options: ReadableMap?, promise: Promise) {
112
+ try {
113
+ val enableLogging =
114
+ options?.let { if (it.hasKey("enableLogging")) it.getBoolean("enableLogging") else true } ?: true
115
+ val config = io.billdog.core.BillDogConfig(apiKey = apiKey, enableLogging = enableLogging)
116
+ BillDog.configure(reactApplicationContext.applicationContext, apiKey, config)
117
+ registerOptionalModules()
118
+ promise.resolve(true)
119
+ } catch (e: Throwable) {
120
+ promise.reject("CONFIGURE_ERROR", e.message, e)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Register the optional native submodules with the core SDK.
126
+ *
127
+ * WHY THIS EXISTS: billdog-core keeps an OPT-IN module registry —
128
+ * `BillDog.use(module)` (BillDog.kt:415) is the only way to populate it, and
129
+ * `BillDog.configure()` does NOT auto-register anything. Every bridge module
130
+ * here resolves its manager per-call out of `BillDog.getModules()`
131
+ * (BillDog.kt:427), so with an empty registry `BillDogAnalyticsModule` /
132
+ * `BillDogABTestModule` resolve `null` and silently NO-OP: `track()` reports
133
+ * success while `BillDog.trackEvent` drops the event at
134
+ * `findAnalyticsModule()?.track(event)` (BillDog.kt:948), and `getDistinctId()`
135
+ * returns JS `null` even though the Kotlin signature is non-nullable `String`.
136
+ * Verified 2026-07-21 on-device: the RN harness emitted events that NEVER
137
+ * reached ClickHouse.
138
+ *
139
+ * A native Kotlin host does this itself (see the Android Inspector's
140
+ * BillDogBootstrap.kt:145-165) — but an RN app has no Kotlin bootstrap, so the
141
+ * bridge IS the host and must do it here. Mirrors that reference sequence.
142
+ *
143
+ * Idempotent: `use()` is a bare `registeredModules.add()` with NO de-dupe, so
144
+ * a second configure() would register a second analytics module and every
145
+ * event would be sent twice.
146
+ */
147
+ private fun registerOptionalModules() {
148
+ if (!modulesRegistered.compareAndSet(false, true)) return
149
+ val app = reactApplicationContext.applicationContext as? android.app.Application
150
+ try {
151
+ // Analytics. `use()` injects the network client + config, but the event
152
+ // QUEUE is installed separately: track() ends in `val db = database ?:
153
+ // return@launch` (BillDogAnalytics.kt:690) and `database` is only set by
154
+ // initializeAutoTracking(). Registering without this call still drops
155
+ // every event, so both steps are REQUIRED.
156
+ val analytics = io.billdog.analytics.BillDogAnalytics()
157
+ BillDog.use(analytics)
158
+ if (app != null) {
159
+ analytics.initializeAutoTracking(app)
160
+ } else {
161
+ Log.w(TAG, "No Application context — analytics persistence disabled; events will not queue")
162
+ }
163
+
164
+ // A/B test manager — BillDogABTestModule resolves this out of the registry.
165
+ BillDog.use(io.billdog.abtest.BillDogABTest(reactApplicationContext.applicationContext))
166
+ } catch (e: Throwable) {
167
+ // Never fail configure() because an optional module is absent from the
168
+ // host's classpath — the corresponding bridge just keeps no-oping.
169
+ modulesRegistered.set(false)
170
+ Log.w(TAG, "Optional module registration failed: ${e.message}", e)
171
+ }
172
+ }
173
+
174
+ // ── PUR-350 / PUR-364 — Unified purchase API ───────────────────────────
175
+
176
+ /**
177
+ * native.purchase({ ...params, source }) — JS passes a PurchaseParams object.
178
+ * We route on `productId`. Delegates to BillDog.purchase(activity, productId)
179
+ * — BillDog.kt:700 (suspend). Custom-product / package overloads and full
180
+ * PurchaseController EXTERNAL bridging are DEFERRED (see purchaseControllerActive).
181
+ */
182
+ @ReactMethod
183
+ fun purchase(params: ReadableMap, promise: Promise) {
184
+ val productId = if (params.hasKey("productId")) params.getString("productId") else null
185
+ if (productId.isNullOrBlank()) {
186
+ promise.reject("INVALID_ARGS", "Missing productId", null)
187
+ return
188
+ }
189
+ val activity: Activity? = currentActivity
190
+ launch(promise) {
191
+ // Activity-less overload (BillDog.kt:680) is available but only the
192
+ // Activity-aware entry point (BillDog.kt:700) can launch the Play
193
+ // billing sheet; fall back to it when no Activity is attached.
194
+ val result = if (activity != null) {
195
+ BillDog.purchase(activity, productId)
196
+ } else {
197
+ BillDog.purchase(productId)
198
+ }
199
+ serializePurchaseResult(result)
200
+ }
201
+ }
202
+
203
+ /**
204
+ * native.setPurchaseControllerActive(active) — no native equivalent.
205
+ * Stores the flag; full JS-PurchaseController bridging is DEFERRED.
206
+ */
207
+ @ReactMethod
208
+ fun setPurchaseControllerActive(active: Boolean, promise: Promise) {
209
+ purchaseControllerActive = active
210
+ Log.d(TAG, "setPurchaseControllerActive($active) — flag stored; JS-controller bridging deferred")
211
+ promise.resolve(null)
212
+ }
213
+
214
+ /** native.restore() → BillDog.restorePurchases() — BillDog.kt:822 (suspend). */
215
+ @ReactMethod
216
+ fun restore(promise: Promise) {
217
+ launch(promise) { serializeRestoreResult(BillDog.restorePurchases()) }
218
+ }
219
+
220
+ // ── PUR-141/224 — CustomerInfo ─────────────────────────────────────────
221
+
222
+ /**
223
+ * native.getCustomerInfo(policy) → BillDog.getCustomerInfo(forceRefresh)
224
+ * — BillDog.kt:902 (suspend, Result<CustomerInfo>). Only "FETCH_CURRENT"
225
+ * maps to forceRefresh=true (mirrors the Flutter plugin).
226
+ */
227
+ @ReactMethod
228
+ fun getCustomerInfo(policy: String?, promise: Promise) {
229
+ val forceRefresh = policy == "FETCH_CURRENT"
230
+ launch(promise) {
231
+ val info = BillDog.getCustomerInfo(forceRefresh).getOrElse { throw it }
232
+ serializeCustomerInfo(info)
233
+ }
234
+ }
235
+
236
+ /** native.invalidateCustomerInfoCache() → BillDog.invalidateCustomerInfoCache() — BillDog.kt:2516 (suspend). */
237
+ @ReactMethod
238
+ fun invalidateCustomerInfoCache(promise: Promise) {
239
+ launch(promise) { BillDog.invalidateCustomerInfoCache(); null }
240
+ }
241
+
242
+ /** native.clearCache() → BillDog.clearCache() — BillDog.kt:1490. */
243
+ @ReactMethod
244
+ fun clearCache(promise: Promise) {
245
+ try {
246
+ BillDog.clearCache()
247
+ promise.resolve(null)
248
+ } catch (e: Throwable) {
249
+ promise.reject("BILLDOG_ERROR", e.message, e)
250
+ }
251
+ }
252
+
253
+ // ── Store capability surface ────────────────────────────────────────────
254
+
255
+ /** native.canMakePayments() → BillDog.canMakePayments() — BillDog.kt:1296 (Boolean). */
256
+ @ReactMethod
257
+ fun canMakePayments(promise: Promise) {
258
+ try {
259
+ promise.resolve(BillDog.canMakePayments())
260
+ } catch (e: Throwable) {
261
+ promise.reject("BILLDOG_ERROR", e.message, e)
262
+ }
263
+ }
264
+
265
+ /**
266
+ * native.presentCodeRedemptionSheet() — NO Android equivalent (iOS-only App
267
+ * Store promo-code sheet). Resolve null (documented no-op).
268
+ */
269
+ @ReactMethod
270
+ fun presentCodeRedemptionSheet(promise: Promise) {
271
+ Log.d(TAG, "presentCodeRedemptionSheet() — iOS-only; no Android equivalent; no-op")
272
+ promise.resolve(null)
273
+ }
274
+
275
+ /**
276
+ * native.checkTrialOrIntroDiscountEligibility(productIds) — NO native
277
+ * equivalent in billdog-core (no matching method). Resolve a map of
278
+ * productId → "unknown" (mirrors the JS fallback shape).
279
+ */
280
+ @ReactMethod
281
+ fun checkTrialOrIntroDiscountEligibility(productIds: ReadableArray, promise: Promise) {
282
+ val out = Arguments.createMap()
283
+ for (i in 0 until productIds.size()) {
284
+ productIds.getString(i)?.let { out.putString(it, "unknown") }
285
+ }
286
+ Log.d(TAG, "checkTrialOrIntroDiscountEligibility — no native equivalent; returning 'unknown'")
287
+ promise.resolve(out)
288
+ }
289
+
290
+ /**
291
+ * native.subscribeStorefront() — NO native equivalent (iOS-only storefront
292
+ * country observer; Play exposes no country API). Documented no-op.
293
+ */
294
+ @ReactMethod
295
+ fun subscribeStorefront(promise: Promise) {
296
+ Log.d(TAG, "subscribeStorefront() — iOS-only; no Android equivalent; no-op")
297
+ promise.resolve(null)
298
+ }
299
+
300
+ // ── PUR-362 — Self-hosted proxy ─────────────────────────────────────────
301
+
302
+ /** native.setProxyURL(url) → BillDog.setProxyURL(url) — BillDog.kt:1349. */
303
+ @ReactMethod
304
+ fun setProxyURL(url: String?, promise: Promise) {
305
+ BillDog.setProxyURL(url)
306
+ promise.resolve(null)
307
+ }
308
+
309
+ /**
310
+ * native.getProxyURL() → BillDog.getProxyURL() — BillDog.kt:1355 (String?).
311
+ * BillDog.ts reads this synchronously (`native.getProxyURL() ?? null`), so it must be a
312
+ * blocking-synchronous method (a Promise would return a truthy object, never the value).
313
+ */
314
+ @ReactMethod(isBlockingSynchronousMethod = true)
315
+ fun getProxyURL(): String? = BillDog.getProxyURL()
316
+
317
+ // ── Consent (cross-SDK contract) ────────────────────────────────────────
318
+
319
+ /** native.setConsentRequired(required) → BillDog.setConsentRequired(required) — BillDog.kt:1485. */
320
+ @ReactMethod
321
+ fun setConsentRequired(required: Boolean, promise: Promise) {
322
+ BillDog.setConsentRequired(required)
323
+ promise.resolve(null)
324
+ }
325
+
326
+ /**
327
+ * native.setConsent(purpose, granted) — NO per-purpose native equivalent;
328
+ * billdog-core only exposes the global setConsentGiven (BillDog.kt:1486).
329
+ * Forward as the global toggle when granted for any purpose is set, else
330
+ * log. Documented mapping limitation; resolve null.
331
+ */
332
+ @ReactMethod
333
+ fun setConsent(purpose: String?, granted: Boolean, promise: Promise) {
334
+ // billdog-core has no per-purpose consent API; the closest primitive is
335
+ // the global consent flag. We do NOT collapse per-purpose into global to
336
+ // avoid over-granting — this is a documented no-op pending a native
337
+ // per-purpose ConsentManager API.
338
+ Log.d(TAG, "setConsent(purpose=$purpose, granted=$granted) — no per-purpose native API; no-op")
339
+ promise.resolve(null)
340
+ }
341
+
342
+ /** native.setConsentGiven(given) → BillDog.setConsentGiven(given) — BillDog.kt:1486. */
343
+ @ReactMethod
344
+ fun setConsentGiven(given: Boolean, promise: Promise) {
345
+ BillDog.setConsentGiven(given)
346
+ promise.resolve(null)
347
+ }
348
+
349
+ /**
350
+ * native.consentHeaderValue() — NO native accessor in billdog-core (consent
351
+ * state is applied to requests internally, not exposed as a header string).
352
+ * Resolve null (documented no-op).
353
+ */
354
+ @ReactMethod
355
+ fun consentHeaderValue(promise: Promise) {
356
+ Log.d(TAG, "consentHeaderValue() — no native accessor; returning null")
357
+ promise.resolve(null)
358
+ }
359
+
360
+ // ── PUR-360 — attribution / device identifiers ──────────────────────────
361
+
362
+ /**
363
+ * native.collectDeviceIdentifiers() → BillDog.collectDeviceIdentifiers()
364
+ * — BillDog.kt:1367 (suspend, returns DeviceIdentifiers). JS treats it as
365
+ * void; we resolve null on completion.
366
+ */
367
+ @ReactMethod
368
+ fun collectDeviceIdentifiers(promise: Promise) {
369
+ launch(promise) { BillDog.collectDeviceIdentifiers(); null }
370
+ }
371
+
372
+ /**
373
+ * native.setAttribution({ key: id, … }) → the matching native provider setter
374
+ * (BillDog.kt:2646+). Keys are provider attribution keys; each maps 1:1 to a
375
+ * consent-gated native setter. Unknown keys are ignored. The JS facade's
376
+ * named setters (setMixpanelDistinctID etc.) all funnel through here.
377
+ */
378
+ @ReactMethod
379
+ fun setAttribution(data: ReadableMap, promise: Promise) {
380
+ launch(promise) {
381
+ val it = data.keySetIterator()
382
+ while (it.hasNextKey()) {
383
+ val key = it.nextKey()
384
+ val value = data.getString(key) ?: continue
385
+ when (key) {
386
+ "cleverTapId" -> BillDog.setCleverTapID(value)
387
+ "mixpanelDistinctId" -> BillDog.setMixpanelDistinctID(value)
388
+ "onesignalId" -> BillDog.setOnesignalID(value)
389
+ "fbAnonymousId" -> BillDog.setFBAnonymousID(value)
390
+ "airshipChannelId" -> BillDog.setAirshipChannelID(value)
391
+ "moEngageAnonymousId" -> BillDog.setMoEngageAnonymousID(value)
392
+ "mparticleId" -> BillDog.setMparticleID(value)
393
+ "kochavaId" -> BillDog.setKochavaDeviceID(value)
394
+ "tenjinId" -> BillDog.setTenjinAnalyticsInstallationID(value)
395
+ "iterableId" -> BillDog.setIterableUserID(value)
396
+ "segmentId" -> BillDog.setSegmentAnonymousID(value)
397
+ // MMP providers — dedicated native setters.
398
+ "adjustId" -> BillDog.setAdjustId(value)
399
+ "appsflyerId" -> BillDog.setAppsflyerId(value)
400
+ "firebaseAppInstanceId" -> BillDog.setFirebaseAppInstanceId(value)
401
+ else -> { /* unknown provider key — ignore */ }
402
+ }
403
+ }
404
+ null
405
+ }
406
+ }
407
+
408
+ // ── Identity API ────────────────────────────────────────────────────────
409
+
410
+ /** native.logIn(appUserId) → BillDog.logIn(appUserId) — BillDog.kt:1169 (suspend, Result<LogInResult>). */
411
+ @ReactMethod
412
+ fun logIn(appUserId: String, promise: Promise) {
413
+ launch(promise) {
414
+ BillDog.logIn(appUserId).exceptionOrNull()?.let { throw it }
415
+ null
416
+ }
417
+ }
418
+
419
+ /** native.logOut() → BillDog.logOut() — BillDog.kt:1209. */
420
+ @ReactMethod
421
+ fun logOut(promise: Promise) {
422
+ try {
423
+ BillDog.logOut()
424
+ promise.resolve(null)
425
+ } catch (e: Throwable) {
426
+ promise.reject("BILLDOG_ERROR", e.message, e)
427
+ }
428
+ }
429
+
430
+ /**
431
+ * native.setSandboxMode(enabled) — NO native setter. Sandbox is derived from
432
+ * the API-key prefix (bd_public_test_* / bd_secret_test_*); documented no-op.
433
+ */
434
+ @ReactMethod
435
+ fun setSandboxMode(enabled: Boolean, promise: Promise) {
436
+ Log.d(TAG, "setSandboxMode($enabled) — no native setter (sandbox tracked by API-key prefix); no-op")
437
+ promise.resolve(null)
438
+ }
439
+
440
+ // ── Placement registration ──────────────────────────────────────────────
441
+
442
+ /**
443
+ * native.registerPlacement({ placementId, params }) → BillDog.register(
444
+ * placementId, activity, params, handler) — BillDog.kt:1377 (suspend).
445
+ * Requires an Activity; rejects "NO_ACTIVITY" when currentActivity is null.
446
+ */
447
+ @ReactMethod
448
+ fun registerPlacement(args: ReadableMap, promise: Promise) {
449
+ val placementId = if (args.hasKey("placementId")) args.getString("placementId") else null
450
+ if (placementId.isNullOrBlank()) {
451
+ promise.reject("INVALID_ARGS", "Missing placementId", null)
452
+ return
453
+ }
454
+ val activity = currentActivity
455
+ ?: return promise.reject("NO_ACTIVITY", "No Activity available for registerPlacement", null)
456
+
457
+ @Suppress("UNCHECKED_CAST")
458
+ val params: Map<String, Any>? =
459
+ if (args.hasKey("params")) args.getMap("params")?.toHashMap() as? Map<String, Any> else null
460
+
461
+ val replied = AtomicBoolean(false)
462
+ scope.launch {
463
+ try {
464
+ BillDog.register(placementId, activity, params) { placementResult ->
465
+ if (replied.compareAndSet(false, true)) {
466
+ promise.resolve(serializePlacementResult(placementResult))
467
+ }
468
+ }
469
+ } catch (e: Throwable) {
470
+ if (replied.compareAndSet(false, true)) {
471
+ promise.reject("BILLDOG_ERROR", e.message, e)
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ /**
478
+ * native.hasAccessForPlacement(placementId) → BillDog.hasAccessForPlacement — BillDog.kt:1440 (Boolean).
479
+ * BillDog.ts reads this synchronously (`!!native.hasAccessForPlacement(id)`), so it must be
480
+ * blocking-synchronous. On error, default false (JS treats a thrown/false as "no access").
481
+ */
482
+ @ReactMethod(isBlockingSynchronousMethod = true)
483
+ fun hasAccessForPlacement(placementId: String): Boolean =
484
+ try { BillDog.hasAccessForPlacement(placementId) } catch (e: Throwable) { false }
485
+
486
+ /**
487
+ * native.getPresentationResult(placementId) → BillDog.getPresentationResult(
488
+ * placementId) — BillDog.kt:2842 (suspend).
489
+ */
490
+ @ReactMethod
491
+ fun getPresentationResult(placementId: String, promise: Promise) {
492
+ launch(promise) { serializePresentationResult(BillDog.getPresentationResult(placementId)) }
493
+ }
494
+
495
+ // Experiments & feature flags: the paywall-facade shim was removed. Use the
496
+ // standalone BillDogABTest module (bridged separately) instead.
497
+
498
+ // ── In-app messages ─────────────────────────────────────────────────────
499
+
500
+ /**
501
+ * native.showInAppMessages(placementId?) → BillDog.showInAppMessages(activity)
502
+ * — BillDog.kt:2873. Native has no placement param; placementId is ignored.
503
+ * Requires an Activity → reject "NO_ACTIVITY" when null.
504
+ */
505
+ @ReactMethod
506
+ fun showInAppMessages(placementId: String?, promise: Promise) {
507
+ val activity = currentActivity
508
+ ?: return promise.reject("NO_ACTIVITY", "No Activity available for showInAppMessages", null)
509
+ try {
510
+ BillDog.showInAppMessages(activity)
511
+ promise.resolve(null)
512
+ } catch (e: Throwable) {
513
+ promise.reject("BILLDOG_ERROR", e.message, e)
514
+ }
515
+ }
516
+
517
+ // ── Deep linking ────────────────────────────────────────────────────────
518
+
519
+ /**
520
+ * native.handleDeepLink(uri) → BillDog.handleDeepLink(Uri.parse(uri))
521
+ * — BillDog.kt:883 (returns DeepLinkResult?). Resolve true when handled.
522
+ */
523
+ @ReactMethod
524
+ fun handleDeepLink(uri: String, promise: Promise) {
525
+ val handled = try {
526
+ BillDog.handleDeepLink(Uri.parse(uri)) != null
527
+ } catch (e: Exception) {
528
+ Log.w(TAG, "handleDeepLink failed: ${e.message}")
529
+ false
530
+ }
531
+ promise.resolve(handled)
532
+ }
533
+
534
+ // ── Component selection state ───────────────────────────────────────────
535
+
536
+ /**
537
+ * native.getSelectedProductIndex(componentId) → BillDog.getSelectedProductIndex — BillDog.kt:1448 (Int?).
538
+ * BillDog.ts reads this synchronously, so it must be blocking-synchronous.
539
+ */
540
+ @ReactMethod(isBlockingSynchronousMethod = true)
541
+ fun getSelectedProductIndex(componentId: String): Int? =
542
+ BillDog.getSelectedProductIndex(componentId)
543
+
544
+ /** native.setSelectedProductIndex(componentId, index) → BillDog.setSelectedProductIndex(componentId, index) — BillDog.kt:1454. */
545
+ @ReactMethod
546
+ fun setSelectedProductIndex(componentId: String, index: Int, promise: Promise) {
547
+ BillDog.setSelectedProductIndex(componentId, index)
548
+ promise.resolve(null)
549
+ }
550
+
551
+ // ── Theme / interface style ─────────────────────────────────────────────
552
+
553
+ /** native.updateTheme(overrides) → BillDog.updateTheme(overrides) — BillDog.kt:212. */
554
+ @ReactMethod
555
+ fun updateTheme(overrides: ReadableMap, promise: Promise) {
556
+ val map = overrides.toHashMap().entries
557
+ .filter { it.value != null }
558
+ .associate { it.key to it.value.toString() }
559
+ BillDog.updateTheme(map)
560
+ promise.resolve(null)
561
+ }
562
+
563
+ /** native.setInterfaceStyle(style) → BillDog.setInterfaceStyle(InterfaceStyle?) — BillDog.kt:2604. null = follow system. */
564
+ @ReactMethod
565
+ fun setInterfaceStyle(style: String?, promise: Promise) {
566
+ BillDog.setInterfaceStyle(parseInterfaceStyle(style))
567
+ promise.resolve(null)
568
+ }
569
+
570
+ // ── Subscription management ─────────────────────────────────────────────
571
+
572
+ /**
573
+ * native.showManageSubscriptions(subscriptionId?) → BillDog.showManageSubscriptions(
574
+ * activity, subscriptionId) — BillDog.kt:2035. Requires an Activity → reject
575
+ * "NO_ACTIVITY" when null.
576
+ */
577
+ @ReactMethod
578
+ fun showManageSubscriptions(subscriptionId: String?, promise: Promise) {
579
+ val activity = currentActivity
580
+ ?: return promise.reject("NO_ACTIVITY", "No Activity available for showManageSubscriptions", null)
581
+ try {
582
+ BillDog.showManageSubscriptions(activity, subscriptionId)
583
+ promise.resolve(null)
584
+ } catch (e: Throwable) {
585
+ promise.reject("BILLDOG_ERROR", e.message, e)
586
+ }
587
+ }
588
+
589
+ // ── User attributes ─────────────────────────────────────────────────────
590
+
591
+ /** native.setUserAttributes(attributes) → BillDog.setUserAttributes(attributes) — BillDog.kt:2618 (suspend, Result<Unit>). */
592
+ @ReactMethod
593
+ fun setUserAttributes(attributes: ReadableMap, promise: Promise) {
594
+ val map: Map<String, Any?> = attributes.toHashMap()
595
+ launch(promise) {
596
+ BillDog.setUserAttributes(map).exceptionOrNull()?.let { throw it }
597
+ null
598
+ }
599
+ }
600
+
601
+ /** native.getUserAttributes() → BillDog.getUserAttributes() — BillDog.kt:2633 (Map<String, Any>). */
602
+ @ReactMethod
603
+ fun getUserAttributes(promise: Promise) {
604
+ try {
605
+ promise.resolve(toWritableMap(BillDog.getUserAttributes()))
606
+ } catch (e: Throwable) {
607
+ promise.reject("BILLDOG_ERROR", e.message, e)
608
+ }
609
+ }
610
+
611
+ // ── External purchase observation ───────────────────────────────────────
612
+
613
+ /** native.observePurchaseStart(productId) → BillDog.observePurchaseStart(productId) — BillDog.kt:2768. */
614
+ @ReactMethod
615
+ fun observePurchaseStart(productId: String, promise: Promise) {
616
+ BillDog.observePurchaseStart(productId)
617
+ promise.resolve(null)
618
+ }
619
+
620
+ /** native.observePurchaseResult(productId, orderId?) → BillDog.observePurchaseResult(productId, orderId) — BillDog.kt:2774. */
621
+ @ReactMethod
622
+ fun observePurchaseResult(productId: String, orderId: String?, promise: Promise) {
623
+ BillDog.observePurchaseResult(productId, orderId)
624
+ promise.resolve(null)
625
+ }
626
+
627
+ /** native.observePurchaseError(productId, error) → BillDog.observePurchaseError(productId, error) — BillDog.kt:2782. */
628
+ @ReactMethod
629
+ fun observePurchaseError(productId: String, error: String, promise: Promise) {
630
+ BillDog.observePurchaseError(productId, error)
631
+ promise.resolve(null)
632
+ }
633
+
634
+ // ── Server-side tap actions ─────────────────────────────────────────────
635
+
636
+ /**
637
+ * native.executeAction(paywallId, componentId, actionType, idempotencyKey,
638
+ * metadata) — NO native equivalent in billdog-core (Route B tap-action
639
+ * execution has no exposed SDK method). The JS facade already has a direct
640
+ * fetch fallback to the `execute-paywall-action` Edge Function when native is
641
+ * absent, so we resolve null here to signal "no native handler" and let the
642
+ * JS fallback path run. Documented no-op (does NOT reject).
643
+ */
644
+ @ReactMethod
645
+ fun executeAction(
646
+ paywallId: String?,
647
+ componentId: String,
648
+ actionType: String,
649
+ idempotencyKey: String?,
650
+ metadata: ReadableMap?,
651
+ promise: Promise
652
+ ) {
653
+ Log.d(TAG, "executeAction($actionType) — no native SDK method; resolving null (JS fetch fallback handles it)")
654
+ promise.resolve(null)
655
+ }
656
+
657
+ // ── Serializers (shapes match the JS *.ts result unions) ────────────────
658
+
659
+ /**
660
+ * PurchaseResult (PurchaseResult.kt:28) → JS PurchaseResult union.
661
+ * type ∈ purchased | cancelled | pending | failed | restored.
662
+ */
663
+ private fun serializePurchaseResult(r: PurchaseResult): WritableMap = when (r) {
664
+ is PurchaseResult.Purchased -> toWritableMap(mapOf(
665
+ "type" to "purchased",
666
+ "productId" to r.productId,
667
+ "transactionId" to r.transactionId,
668
+ "originalTransactionId" to r.originalTransactionId
669
+ ))
670
+ is PurchaseResult.Cancelled -> toWritableMap(mapOf("type" to "cancelled"))
671
+ is PurchaseResult.Pending -> toWritableMap(mapOf("type" to "pending"))
672
+ is PurchaseResult.Failed -> toWritableMap(mapOf(
673
+ "type" to "failed",
674
+ "error" to mapOf(
675
+ "category" to r.error.category,
676
+ "message" to r.error.message
677
+ )
678
+ ))
679
+ is PurchaseResult.Restored -> toWritableMap(mapOf(
680
+ "type" to "restored",
681
+ "productId" to r.productId,
682
+ "transactionId" to r.transactionId,
683
+ "originalTransactionId" to r.originalTransactionId
684
+ ))
685
+ }
686
+
687
+ /** RestoreResult (PurchaseResult.kt:60) → JS RestoreResult union. */
688
+ private fun serializeRestoreResult(r: RestoreResult): WritableMap = when (r) {
689
+ is RestoreResult.Restored -> toWritableMap(mapOf(
690
+ "type" to "restored",
691
+ "items" to r.items.map {
692
+ mapOf(
693
+ "productId" to it.productId,
694
+ "transactionId" to it.transactionId,
695
+ "originalTransactionId" to it.originalTransactionId
696
+ )
697
+ }
698
+ ))
699
+ is RestoreResult.Failed -> toWritableMap(mapOf(
700
+ "type" to "failed",
701
+ "error" to mapOf(
702
+ "category" to r.error.category,
703
+ "message" to r.error.message
704
+ )
705
+ ))
706
+ }
707
+
708
+ /** PlacementResult (PlacementManager.kt) → JS PlacementResult union. */
709
+ private fun serializePlacementResult(r: io.billdog.managers.PlacementResult): WritableMap = when (r) {
710
+ is io.billdog.managers.PlacementResult.Entitled -> toWritableMap(mapOf("type" to "entitled"))
711
+ is io.billdog.managers.PlacementResult.Purchased ->
712
+ toWritableMap(mapOf("type" to "purchased", "productId" to r.productId))
713
+ is io.billdog.managers.PlacementResult.Dismissed -> toWritableMap(mapOf("type" to "dismissed"))
714
+ is io.billdog.managers.PlacementResult.Restored -> toWritableMap(mapOf("type" to "restored"))
715
+ is io.billdog.managers.PlacementResult.Error ->
716
+ toWritableMap(mapOf("type" to "error", "message" to (r.exception.message ?: "Placement error")))
717
+ }
718
+
719
+ /** PresentationResult (BillDog.kt) → JS PresentationResult union. */
720
+ private fun serializePresentationResult(r: PresentationResult): WritableMap = when (r) {
721
+ is PresentationResult.Paywall -> toWritableMap(mapOf("type" to "paywall"))
722
+ is PresentationResult.UserSubscribed -> toWritableMap(mapOf("type" to "userSubscribed"))
723
+ is PresentationResult.NoPaywall -> toWritableMap(mapOf("type" to "noPaywall", "reason" to r.reason))
724
+ is PresentationResult.Holdout -> toWritableMap(mapOf("type" to "holdout"))
725
+ is PresentationResult.Error -> toWritableMap(mapOf("type" to "error", "message" to r.message))
726
+ }
727
+
728
+ /** EntitlementInfo (CustomerModels.kt) → JS EntitlementInfo. */
729
+ private fun serializeEntitlement(e: EntitlementInfo): Map<String, Any?> = mapOf(
730
+ "identifier" to e.identifier,
731
+ "isActive" to e.isActive,
732
+ "expiresAt" to e.expiresAt,
733
+ "store" to e.store,
734
+ "grantedAt" to e.grantedAt,
735
+ "willRenew" to e.willRenew,
736
+ "isLifetime" to e.isLifetime,
737
+ "state" to e.state,
738
+ "productIds" to e.productIds,
739
+ "startsAt" to e.startsAt,
740
+ "renewedAt" to e.renewedAt,
741
+ "latestProductId" to e.latestProductId
742
+ )
743
+
744
+ /**
745
+ * CustomerInfo (CustomerModels.kt) → JS CustomerInfo. `subscriptions` is
746
+ * emitted EMPTY — native activeSubscriptions is only a List<String> of ids
747
+ * with no per-subscription dates (mirrors the Flutter plugin limitation);
748
+ * active state is carried by `entitlements`.
749
+ */
750
+ private fun serializeCustomerInfo(info: CustomerInfo): WritableMap = toWritableMap(mapOf(
751
+ "userId" to info.userId,
752
+ "originalAppUserId" to info.originalAppUserId,
753
+ "entitlements" to info.entitlements.map { serializeEntitlement(it) },
754
+ "subscriptions" to emptyList<Any?>(),
755
+ "productEntitlementMapping" to info.productEntitlementMapping
756
+ ))
757
+
758
+ // ── Parsers ──────────────────────────────────────────────────────────────
759
+
760
+ private fun parseInterfaceStyle(value: String?): InterfaceStyle? =
761
+ when (value?.lowercase()) {
762
+ "light" -> InterfaceStyle.LIGHT
763
+ "dark" -> InterfaceStyle.DARK
764
+ "system" -> InterfaceStyle.SYSTEM
765
+ else -> null
766
+ }
767
+
768
+ // ── Map<String, Any?> → WritableMap converter ────────────────────────────
769
+ // RN's Writable* types are the only values the bridge can send to JS. This
770
+ // recursively converts plain Kotlin maps/lists/scalars produced by the
771
+ // serializers above. Longs/Floats are widened to Double (JS number).
772
+
773
+ private fun toWritableMap(map: Map<String, Any?>): WritableMap {
774
+ val out = Arguments.createMap()
775
+ for ((key, value) in map) {
776
+ when (value) {
777
+ null -> out.putNull(key)
778
+ is Boolean -> out.putBoolean(key, value)
779
+ is Int -> out.putInt(key, value)
780
+ is Long -> out.putDouble(key, value.toDouble())
781
+ is Float -> out.putDouble(key, value.toDouble())
782
+ is Double -> out.putDouble(key, value)
783
+ is String -> out.putString(key, value)
784
+ is WritableMap -> out.putMap(key, value)
785
+ is WritableArray -> out.putArray(key, value)
786
+ is Map<*, *> -> out.putMap(key, toWritableMap(coerceStringKeys(value)))
787
+ is List<*> -> out.putArray(key, toWritableArray(value))
788
+ else -> out.putString(key, value.toString())
789
+ }
790
+ }
791
+ return out
792
+ }
793
+
794
+ private fun toWritableArray(list: List<*>): WritableArray {
795
+ val out = Arguments.createArray()
796
+ for (value in list) {
797
+ when (value) {
798
+ null -> out.pushNull()
799
+ is Boolean -> out.pushBoolean(value)
800
+ is Int -> out.pushInt(value)
801
+ is Long -> out.pushDouble(value.toDouble())
802
+ is Float -> out.pushDouble(value.toDouble())
803
+ is Double -> out.pushDouble(value)
804
+ is String -> out.pushString(value)
805
+ is WritableMap -> out.pushMap(value)
806
+ is WritableArray -> out.pushArray(value)
807
+ is Map<*, *> -> out.pushMap(toWritableMap(coerceStringKeys(value)))
808
+ is List<*> -> out.pushArray(toWritableArray(value))
809
+ else -> out.pushString(value.toString())
810
+ }
811
+ }
812
+ return out
813
+ }
814
+
815
+ private fun coerceStringKeys(map: Map<*, *>): Map<String, Any?> =
816
+ map.entries.associate { (k, v) -> k.toString() to v }
817
+ }