@scalebun-release/react-native 2.5.0

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 (1203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -0
  3. package/android/build.gradle +94 -0
  4. package/android/consumer-rules.pro +28 -0
  5. package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
  6. package/android/src/main/AndroidManifest.xml +14 -0
  7. package/android/src/main/java/com/scalebun/core/crash/CrashStorage.kt +155 -0
  8. package/android/src/main/java/com/scalebun/core/crash/NativeCrashHandler.kt +93 -0
  9. package/android/src/main/java/com/scalebun/core/performance/ANRWatchdog.kt +100 -0
  10. package/android/src/main/java/com/scalebun/core/performance/AppStartCollector.kt +180 -0
  11. package/android/src/main/java/com/scalebun/core/performance/FrameMetricsCollector.kt +230 -0
  12. package/android/src/main/java/com/scalebun/core/performance/PerformanceCore.kt +165 -0
  13. package/android/src/main/java/com/scalebun/profiler/ProfilerModule.kt +398 -0
  14. package/android/src/main/java/com/scalebun/profiler/ProfilerPackage.kt +24 -0
  15. package/android/src/main/java/com/scalebun/profiler/collectors/CpuCollector.kt +161 -0
  16. package/android/src/main/java/com/scalebun/profiler/collectors/IncidentDetector.kt +223 -0
  17. package/android/src/main/java/com/scalebun/profiler/collectors/MemoryCollector.kt +157 -0
  18. package/android/src/main/java/com/scalebun/profiler/collectors/RenderCollector.kt +194 -0
  19. package/android/src/main/java/com/scalebun/profiler/collectors/SamplerScheduler.kt +218 -0
  20. package/android/src/main/java/com/scalebun/profiler/collectors/ThreadCollector.kt +142 -0
  21. package/android/src/main/java/com/scalebun/replaysdk/ReplaySdkModule.kt +1117 -0
  22. package/android/src/main/java/com/scalebun/replaysdk/ReplaySdkPackage.kt +24 -0
  23. package/android/src/main/java/com/scalebun/replaysdk/capture/CaptureReason.kt +47 -0
  24. package/android/src/main/java/com/scalebun/replaysdk/capture/FrameCaptureManager.kt +574 -0
  25. package/android/src/main/java/com/scalebun/replaysdk/capture/FrameCaptureScheduler.kt +187 -0
  26. package/android/src/main/java/com/scalebun/replaysdk/capture/UiStabilityDetector.kt +214 -0
  27. package/android/src/main/java/com/scalebun/replaysdk/core/ReplayConfig.kt +144 -0
  28. package/android/src/main/java/com/scalebun/replaysdk/core/ReplayLogger.kt +31 -0
  29. package/android/src/main/java/com/scalebun/replaysdk/core/SessionIdGenerator.kt +15 -0
  30. package/android/src/main/java/com/scalebun/replaysdk/models/ReplayModels.kt +94 -0
  31. package/android/src/main/java/com/scalebun/replaysdk/outbox/NativeOutbox.kt +574 -0
  32. package/android/src/main/java/com/scalebun/replaysdk/outbox/OutboxUploader.kt +559 -0
  33. package/android/src/main/java/com/scalebun/replaysdk/privacy/PrivacyMaskProcessor.kt +126 -0
  34. package/android/src/main/java/com/scalebun/replaysdk/session/SessionManager.kt +322 -0
  35. package/android/src/main/java/com/scalebun/replaysdk/storage/ReplayStorage.kt +136 -0
  36. package/android/src/main/java/com/scalebun/replaysdk/timeline/TimelineCollector.kt +28 -0
  37. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +470 -0
  38. package/android/src/main/java/com/scalebun/replaysdk/tracking/ScreenTracker.kt +20 -0
  39. package/android/src/main/java/com/scalebun/replaysdk/tracking/ScrollContextProbe.kt +188 -0
  40. package/android/src/main/java/com/scalebun/replaysdk/transport/ReplayTransport.kt +99 -0
  41. package/android/src/main/java/com/scalebun/replaysdk/voice/VoiceRecorder.kt +121 -0
  42. package/android/src/main/java/com/scalebun/replaysdk/voice/VoiceTranscriber.kt +170 -0
  43. package/android/src/main/java/com/scalebun/rn/ScaleBunPackage.kt +113 -0
  44. package/android/src/main/java/com/scalebun/rn/crash/ScaleBunCrashModule.kt +188 -0
  45. package/android/src/main/java/com/scalebun/rn/crash/ScaleBunCrashPackage.kt +22 -0
  46. package/android/src/main/java/com/scalebun/rn/engage/ScaleBunEngageModule.kt +579 -0
  47. package/android/src/main/java/com/scalebun/rn/engage/ScaleBunEngagePackage.kt +22 -0
  48. package/android/src/main/java/com/scalebun/rn/engage/ScaleBunNotificationPresenter.kt +110 -0
  49. package/android/src/main/java/com/scalebun/rn/ota/BsPatch.kt +240 -0
  50. package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +195 -0
  51. package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +81 -0
  52. package/android/src/main/java/com/scalebun/rn/ota/DownloadFailure.kt +55 -0
  53. package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
  54. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
  55. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +683 -0
  56. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +136 -0
  57. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +578 -0
  58. package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
  59. package/android/src/main/java/com/scalebun/rn/performance/PerformanceModule.kt +217 -0
  60. package/android/src/main/java/com/scalebun/rn/referrer/ScaleBunInstallReferrerModule.kt +77 -0
  61. package/android/src/main/java/com/scalebun/rn/storage/ScaleBunStorageModule.kt +68 -0
  62. package/android/src/main/java/com/scalebun/storage/KvFileStore.kt +174 -0
  63. package/android/src/newarch/java/com/scalebun/profiler/ProfilerSpec.kt +12 -0
  64. package/android/src/newarch/java/com/scalebun/replaysdk/ReplaySdkSpec.kt +12 -0
  65. package/android/src/newarch/java/com/scalebun/rn/crash/ScaleBunCrashSpec.kt +15 -0
  66. package/android/src/newarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +16 -0
  67. package/android/src/newarch/java/com/scalebun/rn/performance/PerformanceSpec.kt +12 -0
  68. package/android/src/newarch/java/com/scalebun/rn/referrer/ScaleBunInstallReferrerSpec.kt +14 -0
  69. package/android/src/newarch/java/com/scalebun/rn/storage/ScaleBunStorageSpec.kt +16 -0
  70. package/android/src/oldarch/java/com/scalebun/profiler/ProfilerSpec.kt +34 -0
  71. package/android/src/oldarch/java/com/scalebun/replaysdk/ReplaySdkSpec.kt +77 -0
  72. package/android/src/oldarch/java/com/scalebun/rn/crash/ScaleBunCrashSpec.kt +40 -0
  73. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +42 -0
  74. package/android/src/oldarch/java/com/scalebun/rn/performance/PerformanceSpec.kt +27 -0
  75. package/android/src/oldarch/java/com/scalebun/rn/referrer/ScaleBunInstallReferrerSpec.kt +18 -0
  76. package/android/src/oldarch/java/com/scalebun/rn/storage/ScaleBunStorageSpec.kt +26 -0
  77. package/bin/lib/androidCodemod.js +370 -0
  78. package/bin/lib/iosCodemod.js +343 -0
  79. package/bin/scalebun.js +1004 -0
  80. package/dist/scalebun.full.js +37348 -0
  81. package/dist/scalebun.slim.js +34137 -0
  82. package/ios/Capture/InteractionTracker.swift +237 -0
  83. package/ios/Capture/PrivacyMaskProcessor.swift +158 -0
  84. package/ios/Capture/ScreenshotCaptureManager.swift +398 -0
  85. package/ios/Capture/WindowResolver.swift +63 -0
  86. package/ios/Core/DeviceIdentity.swift +50 -0
  87. package/ios/Core/ReplayConfig.swift +100 -0
  88. package/ios/Core/ReplayLogger.swift +22 -0
  89. package/ios/Core/SessionIdGenerator.swift +18 -0
  90. package/ios/Crash/ScaleBunCrashBridge.mm +74 -0
  91. package/ios/Crash/ScaleBunCrashHandler.swift +84 -0
  92. package/ios/Crash/ScaleBunCrashModule.swift +98 -0
  93. package/ios/Crash/ScaleBunCrashSignalBridge.h +49 -0
  94. package/ios/Crash/ScaleBunCrashSignalBridge.m +296 -0
  95. package/ios/Crash/ScaleBunCrashSignalStress.c +137 -0
  96. package/ios/Crash/ScaleBunCrashStorage.swift +121 -0
  97. package/ios/Crash/ScaleBunExceptionChain.swift +71 -0
  98. package/ios/CrashTests/ScaleBunExceptionChainTests.swift +144 -0
  99. package/ios/Engage/ScaleBunEngageBridge.m +32 -0
  100. package/ios/Engage/ScaleBunEngageModule.swift +250 -0
  101. package/ios/Engage/ScaleBunPushNotificationCenter.swift +185 -0
  102. package/ios/Ota/BsPatch.swift +203 -0
  103. package/ios/Ota/BundleDownloader.swift +249 -0
  104. package/ios/Ota/DeviceIntegrity.swift +112 -0
  105. package/ios/Ota/DownloadFailure.swift +56 -0
  106. package/ios/Ota/OtaProtocol.swift +246 -0
  107. package/ios/Ota/OtaSlotManager.swift +613 -0
  108. package/ios/Ota/ScaleBunOtaBridge.mm +92 -0
  109. package/ios/Ota/ScaleBunOtaEventsModule.swift +61 -0
  110. package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
  111. package/ios/Ota/ScaleBunOtaModule.swift +587 -0
  112. package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +119 -0
  113. package/ios/Ota/TlsPinning.swift +222 -0
  114. package/ios/OtaTests/DownloadFailureTests.swift +78 -0
  115. package/ios/OtaTests/ScaleBunOtaVerifierTests.swift +548 -0
  116. package/ios/Outbox/NativeOutbox.swift +632 -0
  117. package/ios/Outbox/OutboxUploader.swift +578 -0
  118. package/ios/Performance/ScaleBunPerformanceBridge.m +32 -0
  119. package/ios/Performance/ScaleBunPerformanceModule.swift +290 -0
  120. package/ios/PrivacyInfo.xcprivacy +97 -0
  121. package/ios/Profiler/ScaleBunProfilerBridge.m +55 -0
  122. package/ios/Profiler/ScaleBunProfilerModule.swift +966 -0
  123. package/ios/ReplaySdk-Bridging-Header.h +2 -0
  124. package/ios/ReplaySdk.swift +793 -0
  125. package/ios/ReplaySdkBridge.m +97 -0
  126. package/ios/Session/SessionManager.swift +273 -0
  127. package/ios/Skan/ScaleBunSkanBridge.m +14 -0
  128. package/ios/Skan/ScaleBunSkanModule.swift +61 -0
  129. package/ios/Storage/ReplayStorage.swift +61 -0
  130. package/ios/Storage/ScaleBunKvFileStore.swift +125 -0
  131. package/ios/Storage/ScaleBunStorageBridge.mm +72 -0
  132. package/ios/Storage/ScaleBunStorageModule.swift +57 -0
  133. package/ios/Transport/ReplayTransport.swift +70 -0
  134. package/ios/VoiceRecorder.swift +145 -0
  135. package/lib/commonjs/analytics/EventTracker.js +598 -0
  136. package/lib/commonjs/analytics/automaticEvents.js +71 -0
  137. package/lib/commonjs/analytics/batching.js +64 -0
  138. package/lib/commonjs/analytics/eventLane.js +55 -0
  139. package/lib/commonjs/analytics/revenue.js +125 -0
  140. package/lib/commonjs/analytics/subscription.js +94 -0
  141. package/lib/commonjs/bootstrap/FeatureRegistry.js +86 -0
  142. package/lib/commonjs/bootstrap/SDKBootstrapper.js +748 -0
  143. package/lib/commonjs/bucketing/fnv1a32.js +45 -0
  144. package/lib/commonjs/compat/codepush.js +153 -0
  145. package/lib/commonjs/config/ConfigManager.js +238 -0
  146. package/lib/commonjs/config/configTypes.js +16 -0
  147. package/lib/commonjs/config/otaPolicyState.js +85 -0
  148. package/lib/commonjs/config/quotaState.js +435 -0
  149. package/lib/commonjs/core/architecture.js +45 -0
  150. package/lib/commonjs/core/clock/now.js +13 -0
  151. package/lib/commonjs/core/config/defaults.js +2 -0
  152. package/lib/commonjs/core/config/schema.js +512 -0
  153. package/lib/commonjs/core/constants/endpoints.js +19 -0
  154. package/lib/commonjs/core/constants/protocol.js +31 -0
  155. package/lib/commonjs/core/constants/timings.js +39 -0
  156. package/lib/commonjs/core/constants/version.js +18 -0
  157. package/lib/commonjs/core/context/app.js +2 -0
  158. package/lib/commonjs/core/context/device.js +155 -0
  159. package/lib/commonjs/core/context/session.js +2 -0
  160. package/lib/commonjs/core/context/user.js +2 -0
  161. package/lib/commonjs/core/contracts/IFeature.js +2 -0
  162. package/lib/commonjs/core/di/container.js +29 -0
  163. package/lib/commonjs/core/encoding/base64.js +50 -0
  164. package/lib/commonjs/core/id/deviceId.js +52 -0
  165. package/lib/commonjs/core/id/installationId.js +55 -0
  166. package/lib/commonjs/core/id/sessionId.js +2 -0
  167. package/lib/commonjs/core/lifecycle/appLifecycle.js +31 -0
  168. package/lib/commonjs/core/lifecycle/crashSafe.js +33 -0
  169. package/lib/commonjs/core/logger/errorClassification.js +107 -0
  170. package/lib/commonjs/core/logger/internalLogger.js +63 -0
  171. package/lib/commonjs/core/logger/levels.js +2 -0
  172. package/lib/commonjs/crypto/hmacSha256.js +132 -0
  173. package/lib/commonjs/debug/bootstrap.js +429 -0
  174. package/lib/commonjs/debug/commands.js +732 -0
  175. package/lib/commonjs/debug/configBuilder.js +70 -0
  176. package/lib/commonjs/debug/exportBridge.js +155 -0
  177. package/lib/commonjs/debug/hostResolver.js +42 -0
  178. package/lib/commonjs/debug/index.js +141 -0
  179. package/lib/commonjs/debug/metrics.js +39 -0
  180. package/lib/commonjs/debug/perf.js +186 -0
  181. package/lib/commonjs/debug/perfWiring.js +109 -0
  182. package/lib/commonjs/debug/protocol.js +121 -0
  183. package/lib/commonjs/debug/redaction.js +422 -0
  184. package/lib/commonjs/debug/replayWiring.js +149 -0
  185. package/lib/commonjs/debug/screenTracking.js +60 -0
  186. package/lib/commonjs/debug/sessionWiring.js +48 -0
  187. package/lib/commonjs/debug/stream.js +542 -0
  188. package/lib/commonjs/debug/transport.js +368 -0
  189. package/lib/commonjs/features/bugreport/BugReportFeature.js +112 -0
  190. package/lib/commonjs/features/bugreport/buildPayload.js +97 -0
  191. package/lib/commonjs/features/bugreport/index.js +2 -0
  192. package/lib/commonjs/features/crash/CrashFeature.js +86 -0
  193. package/lib/commonjs/features/crash/CrashReporter.js +160 -0
  194. package/lib/commonjs/features/crash/NativeCrashFeature.js +42 -0
  195. package/lib/commonjs/features/crash/capture.js +48 -0
  196. package/lib/commonjs/features/crash/globalHandler.js +41 -0
  197. package/lib/commonjs/features/crash/index.js +14 -0
  198. package/lib/commonjs/features/crash/nativeCrashBridge.js +361 -0
  199. package/lib/commonjs/features/crash/rejectionHandler.js +180 -0
  200. package/lib/commonjs/features/crash/scrubCrash.js +158 -0
  201. package/lib/commonjs/features/engage/EngageAnchor.js +132 -0
  202. package/lib/commonjs/features/engage/EngageArchetypeRenderers.js +4292 -0
  203. package/lib/commonjs/features/engage/EngageInAppView.js +2670 -0
  204. package/lib/commonjs/features/engage/EngageInlinePlacement.js +133 -0
  205. package/lib/commonjs/features/engage/EngagePromptProvider.js +1063 -0
  206. package/lib/commonjs/features/engage/EngagePromptView.js +1252 -0
  207. package/lib/commonjs/features/engage/EngageTransport.js +608 -0
  208. package/lib/commonjs/features/engage/EngageVariantContent.js +1778 -0
  209. package/lib/commonjs/features/engage/__test-support__/reactNativeStub.js +54 -0
  210. package/lib/commonjs/features/engage/attachmentCapture.js +48 -0
  211. package/lib/commonjs/features/engage/captureVoiceAttachment.js +88 -0
  212. package/lib/commonjs/features/engage/engageCoachmarkTour.js +106 -0
  213. package/lib/commonjs/features/engage/engageGameLogic.js +308 -0
  214. package/lib/commonjs/features/engage/engageMediaSizing.js +60 -0
  215. package/lib/commonjs/features/engage/engageMultiStepSheet.js +43 -0
  216. package/lib/commonjs/features/engage/engageOutcome.js +118 -0
  217. package/lib/commonjs/features/engage/engagePushTokenBridge.js +72 -0
  218. package/lib/commonjs/features/engage/engageShakeBridge.js +62 -0
  219. package/lib/commonjs/features/engage/engageSignals.js +39 -0
  220. package/lib/commonjs/features/engage/engageStoreReviewBridge.js +49 -0
  221. package/lib/commonjs/features/engage/engageTestSeenStore.js +99 -0
  222. package/lib/commonjs/features/engage/engageThrottle.js +404 -0
  223. package/lib/commonjs/features/engage/engageTriggerEngine.js +365 -0
  224. package/lib/commonjs/features/engage/engageTypes.js +116 -0
  225. package/lib/commonjs/features/engage/engageVariantResolver.js +143 -0
  226. package/lib/commonjs/features/engage/engageWindowInsets.js +98 -0
  227. package/lib/commonjs/features/engage/gestureTriggerDetector.js +85 -0
  228. package/lib/commonjs/features/engage/transcription/TranscriptionProvider.js +55 -0
  229. package/lib/commonjs/features/install-referrer/installReferrer.js +113 -0
  230. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +1038 -0
  231. package/lib/commonjs/features/journey/ScaleBunFlatList.js +39 -0
  232. package/lib/commonjs/features/journey/ScaleBunImpression.js +113 -0
  233. package/lib/commonjs/features/journey/ScaleBunScrollView.js +102 -0
  234. package/lib/commonjs/features/journey/ScaleBunSectionList.js +39 -0
  235. package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
  236. package/lib/commonjs/features/journey/calibrationContext.js +42 -0
  237. package/lib/commonjs/features/journey/gestureBuffer.js +63 -0
  238. package/lib/commonjs/features/journey/gestureDetector.js +176 -0
  239. package/lib/commonjs/features/journey/impression.js +93 -0
  240. package/lib/commonjs/features/journey/interactionProtocol.js +168 -0
  241. package/lib/commonjs/features/journey/journeyManager.js +466 -0
  242. package/lib/commonjs/features/journey/journeyTypes.js +55 -0
  243. package/lib/commonjs/features/journey/nativeScroll.js +85 -0
  244. package/lib/commonjs/features/journey/navDetector.js +161 -0
  245. package/lib/commonjs/features/journey/screenshotHelper.js +136 -0
  246. package/lib/commonjs/features/journey/scrollContext.js +154 -0
  247. package/lib/commonjs/features/journey/targetGeometry.js +185 -0
  248. package/lib/commonjs/features/journey/targetRegistry.js +53 -0
  249. package/lib/commonjs/features/journey/touchTarget.js +249 -0
  250. package/lib/commonjs/features/journey/uiState.js +188 -0
  251. package/lib/commonjs/features/navigation/AutoScreenDetector.js +607 -0
  252. package/lib/commonjs/features/network/NetworkFeature.js +216 -0
  253. package/lib/commonjs/features/network/index.js +23 -0
  254. package/lib/commonjs/features/network/thirdParty.js +151 -0
  255. package/lib/commonjs/features/ota/OtaEventEmitter.js +129 -0
  256. package/lib/commonjs/features/ota/OtaOrchestrator.js +1309 -0
  257. package/lib/commonjs/features/ota/OtaTypes.js +6 -0
  258. package/lib/commonjs/features/ota/deviceAttributes.js +48 -0
  259. package/lib/commonjs/features/ota/environment.js +172 -0
  260. package/lib/commonjs/features/ota/geoCountry.js +93 -0
  261. package/lib/commonjs/features/ota/retry.js +96 -0
  262. package/lib/commonjs/features/ota/signedDownload.js +187 -0
  263. package/lib/commonjs/features/ota/useOtaUpdate.js +106 -0
  264. package/lib/commonjs/features/performance/PerformanceFeature.js +649 -0
  265. package/lib/commonjs/features/performance/collectors/AppLaunchCollector.js +122 -0
  266. package/lib/commonjs/features/performance/collectors/AutoScreenLoadCollector.js +232 -0
  267. package/lib/commonjs/features/performance/collectors/CustomTraceCollector.js +99 -0
  268. package/lib/commonjs/features/performance/collectors/FrameMetricsCollector.js +184 -0
  269. package/lib/commonjs/features/performance/collectors/JsStallCollector.js +165 -0
  270. package/lib/commonjs/features/performance/collectors/NetworkPerfCollector.js +59 -0
  271. package/lib/commonjs/features/performance/collectors/ScreenLoadCollector.js +78 -0
  272. package/lib/commonjs/features/performance/collectors/UiHangCollector.js +78 -0
  273. package/lib/commonjs/features/performance/config.js +58 -0
  274. package/lib/commonjs/features/performance/getNativePerformanceModule.js +44 -0
  275. package/lib/commonjs/features/performance/index.js +58 -0
  276. package/lib/commonjs/features/performance/metrics/MetricsEngine.js +148 -0
  277. package/lib/commonjs/features/performance/models.js +48 -0
  278. package/lib/commonjs/features/performance/transport/PerformanceTransport.js +168 -0
  279. package/lib/commonjs/features/profiler/ProfilerFeature.js +600 -0
  280. package/lib/commonjs/features/profiler/config.js +151 -0
  281. package/lib/commonjs/features/profiler/contracts.js +62 -0
  282. package/lib/commonjs/features/profiler/index.js +40 -0
  283. package/lib/commonjs/features/profiler/models.js +112 -0
  284. package/lib/commonjs/features/profiler/transport/ProfilerTransport.js +94 -0
  285. package/lib/commonjs/features/replay/bridge/adapters/bridgeAdapter.js +203 -0
  286. package/lib/commonjs/features/replay/bridge/eventEmitter.js +128 -0
  287. package/lib/commonjs/features/replay/bridge/nativeModule.js +95 -0
  288. package/lib/commonjs/features/replay/bridge/nativeQuota.js +121 -0
  289. package/lib/commonjs/features/replay/core/clock/clock.js +47 -0
  290. package/lib/commonjs/features/replay/core/config/defaults.js +88 -0
  291. package/lib/commonjs/features/replay/core/config/validator.js +109 -0
  292. package/lib/commonjs/features/replay/core/env/environmentDetector.js +28 -0
  293. package/lib/commonjs/features/replay/core/errors/safeCall.js +42 -0
  294. package/lib/commonjs/features/replay/core/ids/sessionId.js +53 -0
  295. package/lib/commonjs/features/replay/core/logger/logger.js +36 -0
  296. package/lib/commonjs/features/replay/core/privacy/privacyTypes.js +34 -0
  297. package/lib/commonjs/features/replay/core/queue/boundedQueue.js +71 -0
  298. package/lib/commonjs/features/replay/integrations/alert/alertInstrumentation.js +259 -0
  299. package/lib/commonjs/features/replay/integrations/logs/consoleIntegration.js +139 -0
  300. package/lib/commonjs/features/replay/integrations/network/networkAdapter.js +77 -0
  301. package/lib/commonjs/features/replay/integrations/react-navigation/navigationIntegration.js +88 -0
  302. package/lib/commonjs/features/replay/integrations/touch/ReplayRoot.js +103 -0
  303. package/lib/commonjs/features/replay/pipeline/batching/batchManager.js +119 -0
  304. package/lib/commonjs/features/replay/pipeline/envelope/envelopeBuilder.js +52 -0
  305. package/lib/commonjs/features/replay/pipeline/retry/retryPolicy.js +53 -0
  306. package/lib/commonjs/features/replay/pipeline/transport/transportTypes.js +67 -0
  307. package/lib/commonjs/features/replay/public/api.js +336 -0
  308. package/lib/commonjs/features/replay/public/enums.js +85 -0
  309. package/lib/commonjs/features/replay/public/types.js +6 -0
  310. package/lib/commonjs/features/replay/replay/breadcrumbs/breadcrumbCollector.js +119 -0
  311. package/lib/commonjs/features/replay/replay/frames/frameModels.js +40 -0
  312. package/lib/commonjs/features/replay/replay/session/sessionOrchestrator.js +279 -0
  313. package/lib/commonjs/features/replay/replay/timeline/timelineModels.js +120 -0
  314. package/lib/commonjs/features/replay/transport/BackendReplayTransport.js +175 -0
  315. package/lib/commonjs/features/replay/transport/CompositeReplayTransport.js +75 -0
  316. package/lib/commonjs/features/replay/transport/DesktopReplayTransport.js +124 -0
  317. package/lib/commonjs/features/replay/transport/ReplayTransport.js +2 -0
  318. package/lib/commonjs/features/session/BackendSessionAdapter.js +1915 -0
  319. package/lib/commonjs/features/session/DesktopSessionTransport.js +124 -0
  320. package/lib/commonjs/features/session/JourneyEventPipeline.js +244 -0
  321. package/lib/commonjs/features/session/ReplayCaptureManager.js +1253 -0
  322. package/lib/commonjs/features/session/ScrollTracker.js +147 -0
  323. package/lib/commonjs/features/session/SessionManager.js +1448 -0
  324. package/lib/commonjs/features/session/SyncDecisionEngine.js +95 -0
  325. package/lib/commonjs/features/session/frameLink.js +36 -0
  326. package/lib/commonjs/features/session/frameScrollState.js +41 -0
  327. package/lib/commonjs/features/session/index.js +84 -0
  328. package/lib/commonjs/features/session/nativeCapture.js +53 -0
  329. package/lib/commonjs/features/session/outboxFrameCapture.js +83 -0
  330. package/lib/commonjs/features/session/sessionTypes.js +230 -0
  331. package/lib/commonjs/features/session/viewportPlausibility.js +143 -0
  332. package/lib/commonjs/features/skan/skanBridge.js +54 -0
  333. package/lib/commonjs/features/skan/skanConversionManager.js +134 -0
  334. package/lib/commonjs/index.js +267 -0
  335. package/lib/commonjs/integrations/fetch/fetchInterceptor.js +174 -0
  336. package/lib/commonjs/integrations/navigation/screenTracker.js +2 -0
  337. package/lib/commonjs/integrations/network/bodyCapture.js +336 -0
  338. package/lib/commonjs/integrations/xhr/xhrInterceptor.js +224 -0
  339. package/lib/commonjs/metro/composeSourceMap.js +105 -0
  340. package/lib/commonjs/metro/featureModules.js +131 -0
  341. package/lib/commonjs/metro/index.js +230 -0
  342. package/lib/commonjs/metro/optionalDependencyStub.js +6 -0
  343. package/lib/commonjs/metro/optionalModules.js +31 -0
  344. package/lib/commonjs/metro/serializerCompose.js +161 -0
  345. package/lib/commonjs/pipeline/dispatcher/dispatcher.js +28 -0
  346. package/lib/commonjs/pipeline/envelope/envelope.js +17 -0
  347. package/lib/commonjs/pipeline/envelope/serializer.js +14 -0
  348. package/lib/commonjs/pipeline/processors/dedupe.js +2 -0
  349. package/lib/commonjs/pipeline/processors/enrich.js +16 -0
  350. package/lib/commonjs/pipeline/processors/sampling.js +2 -0
  351. package/lib/commonjs/pipeline/processors/sanitize.js +28 -0
  352. package/lib/commonjs/pipeline/queue/flushQueue.js +107 -0
  353. package/lib/commonjs/pipeline/queue/memoryQueue.js +31 -0
  354. package/lib/commonjs/pipeline/queue/persistentQueue.js +342 -0
  355. package/lib/commonjs/pipeline/queue/queuePolicy.js +2 -0
  356. package/lib/commonjs/pipeline/scheduler/flushScheduler.js +45 -0
  357. package/lib/commonjs/pipeline/scheduler/retryPolicy.js +15 -0
  358. package/lib/commonjs/public/ScaleBunErrorBoundary.js +91 -0
  359. package/lib/commonjs/public/ScaleBunFacade.js +2044 -0
  360. package/lib/commonjs/public/ScaleBunProvider.js +62 -0
  361. package/lib/commonjs/public/typedTracker.js +79 -0
  362. package/lib/commonjs/public/types.js +6 -0
  363. package/lib/commonjs/push/PushManager.js +270 -0
  364. package/lib/commonjs/push/adapters/ManualAdapter.js +86 -0
  365. package/lib/commonjs/push/adapters/NativeBridgeAdapter.js +131 -0
  366. package/lib/commonjs/push/adapters/NoopAdapter.js +45 -0
  367. package/lib/commonjs/push/adapters/RNFirebaseMessagingAdapter.js +248 -0
  368. package/lib/commonjs/push/adapters/loadMessaging.js +29 -0
  369. package/lib/commonjs/push/adapters/loadNotifee.js +36 -0
  370. package/lib/commonjs/push/adapters/selectAdapter.js +153 -0
  371. package/lib/commonjs/push/detect/capabilities.js +136 -0
  372. package/lib/commonjs/push/index.js +33 -0
  373. package/lib/commonjs/push/native/nativeNotifications.js +168 -0
  374. package/lib/commonjs/push/types.js +2 -0
  375. package/lib/commonjs/specs/NativeReplaySdk.js +28 -0
  376. package/lib/commonjs/specs/NativeScaleBunCrash.js +30 -0
  377. package/lib/commonjs/specs/NativeScaleBunInstallReferrer.js +24 -0
  378. package/lib/commonjs/specs/NativeScaleBunOta.js +32 -0
  379. package/lib/commonjs/specs/NativeScaleBunPerformance.js +25 -0
  380. package/lib/commonjs/specs/NativeScaleBunProfiler.js +26 -0
  381. package/lib/commonjs/specs/NativeScaleBunStorage.js +40 -0
  382. package/lib/commonjs/storage/StorageBackend.js +240 -0
  383. package/lib/commonjs/storage/db/sqlite.js +2 -0
  384. package/lib/commonjs/storage/files/fileStore.js +2 -0
  385. package/lib/commonjs/transport/auth/sdkSession.js +28 -0
  386. package/lib/commonjs/transport/backoff/exponentialJitter.js +16 -0
  387. package/lib/commonjs/transport/http/endpoints.js +46 -0
  388. package/lib/commonjs/transport/http/httpClient.js +72 -0
  389. package/lib/commonjs/transport/rateLimit/retryAfter.js +2 -0
  390. package/lib/commonjs/transport/signing/requestSigner.js +2 -0
  391. package/lib/commonjs/vendor/protocol/index.js +327 -0
  392. package/lib/commonjs/vendor/protocol/internal/validation.js +429 -0
  393. package/lib/commonjs/vendor/sdk-contracts/bridge.js +2 -0
  394. package/lib/commonjs/vendor/sdk-contracts/config.js +2 -0
  395. package/lib/commonjs/vendor/sdk-contracts/events.js +2 -0
  396. package/lib/commonjs/vendor/sdk-contracts/features.js +2 -0
  397. package/lib/commonjs/vendor/sdk-contracts/index.js +2 -0
  398. package/lib/commonjs/vendor/sdk-contracts/platform.js +2 -0
  399. package/lib/commonjs/vendor/sdk-contracts/session-boundary.js +6 -0
  400. package/lib/commonjs/vendor/sdk-contracts/session.js +2 -0
  401. package/lib/commonjs/vendor/vendor.manifest +10 -0
  402. package/lib/module/analytics/EventTracker.js +592 -0
  403. package/lib/module/analytics/automaticEvents.js +63 -0
  404. package/lib/module/analytics/batching.js +55 -0
  405. package/lib/module/analytics/eventLane.js +47 -0
  406. package/lib/module/analytics/revenue.js +117 -0
  407. package/lib/module/analytics/subscription.js +86 -0
  408. package/lib/module/bootstrap/FeatureRegistry.js +79 -0
  409. package/lib/module/bootstrap/SDKBootstrapper.js +742 -0
  410. package/lib/module/bucketing/fnv1a32.js +37 -0
  411. package/lib/module/compat/codepush.js +147 -0
  412. package/lib/module/config/ConfigManager.js +230 -0
  413. package/lib/module/config/configTypes.js +10 -0
  414. package/lib/module/config/otaPolicyState.js +76 -0
  415. package/lib/module/config/quotaState.js +423 -0
  416. package/lib/module/core/architecture.js +38 -0
  417. package/lib/module/core/clock/now.js +7 -0
  418. package/lib/module/core/config/defaults.js +2 -0
  419. package/lib/module/core/config/schema.js +506 -0
  420. package/lib/module/core/constants/endpoints.js +13 -0
  421. package/lib/module/core/constants/protocol.js +25 -0
  422. package/lib/module/core/constants/timings.js +26 -0
  423. package/lib/module/core/constants/version.js +12 -0
  424. package/lib/module/core/context/app.js +2 -0
  425. package/lib/module/core/context/device.js +143 -0
  426. package/lib/module/core/context/session.js +2 -0
  427. package/lib/module/core/context/user.js +2 -0
  428. package/lib/module/core/contracts/IFeature.js +2 -0
  429. package/lib/module/core/di/container.js +22 -0
  430. package/lib/module/core/encoding/base64.js +44 -0
  431. package/lib/module/core/id/deviceId.js +45 -0
  432. package/lib/module/core/id/installationId.js +50 -0
  433. package/lib/module/core/id/sessionId.js +2 -0
  434. package/lib/module/core/lifecycle/appLifecycle.js +25 -0
  435. package/lib/module/core/lifecycle/crashSafe.js +26 -0
  436. package/lib/module/core/logger/errorClassification.js +96 -0
  437. package/lib/module/core/logger/internalLogger.js +59 -0
  438. package/lib/module/core/logger/levels.js +2 -0
  439. package/lib/module/crypto/hmacSha256.js +126 -0
  440. package/lib/module/debug/bootstrap.js +393 -0
  441. package/lib/module/debug/commands.js +726 -0
  442. package/lib/module/debug/configBuilder.js +63 -0
  443. package/lib/module/debug/exportBridge.js +148 -0
  444. package/lib/module/debug/hostResolver.js +37 -0
  445. package/lib/module/debug/index.js +13 -0
  446. package/lib/module/debug/metrics.js +33 -0
  447. package/lib/module/debug/perf.js +180 -0
  448. package/lib/module/debug/perfWiring.js +102 -0
  449. package/lib/module/debug/protocol.js +19 -0
  450. package/lib/module/debug/redaction.js +411 -0
  451. package/lib/module/debug/replayWiring.js +139 -0
  452. package/lib/module/debug/screenTracking.js +53 -0
  453. package/lib/module/debug/sessionWiring.js +43 -0
  454. package/lib/module/debug/stream.js +536 -0
  455. package/lib/module/debug/transport.js +362 -0
  456. package/lib/module/features/bugreport/BugReportFeature.js +105 -0
  457. package/lib/module/features/bugreport/buildPayload.js +91 -0
  458. package/lib/module/features/bugreport/index.js +2 -0
  459. package/lib/module/features/crash/CrashFeature.js +79 -0
  460. package/lib/module/features/crash/CrashReporter.js +151 -0
  461. package/lib/module/features/crash/NativeCrashFeature.js +36 -0
  462. package/lib/module/features/crash/capture.js +42 -0
  463. package/lib/module/features/crash/globalHandler.js +36 -0
  464. package/lib/module/features/crash/index.js +8 -0
  465. package/lib/module/features/crash/nativeCrashBridge.js +350 -0
  466. package/lib/module/features/crash/rejectionHandler.js +171 -0
  467. package/lib/module/features/crash/scrubCrash.js +149 -0
  468. package/lib/module/features/engage/EngageAnchor.js +121 -0
  469. package/lib/module/features/engage/EngageArchetypeRenderers.js +4264 -0
  470. package/lib/module/features/engage/EngageInAppView.js +2656 -0
  471. package/lib/module/features/engage/EngageInlinePlacement.js +123 -0
  472. package/lib/module/features/engage/EngagePromptProvider.js +1054 -0
  473. package/lib/module/features/engage/EngagePromptView.js +1243 -0
  474. package/lib/module/features/engage/EngageTransport.js +602 -0
  475. package/lib/module/features/engage/EngageVariantContent.js +1771 -0
  476. package/lib/module/features/engage/__test-support__/reactNativeStub.js +52 -0
  477. package/lib/module/features/engage/attachmentCapture.js +42 -0
  478. package/lib/module/features/engage/captureVoiceAttachment.js +80 -0
  479. package/lib/module/features/engage/engageCoachmarkTour.js +94 -0
  480. package/lib/module/features/engage/engageGameLogic.js +287 -0
  481. package/lib/module/features/engage/engageMediaSizing.js +51 -0
  482. package/lib/module/features/engage/engageMultiStepSheet.js +35 -0
  483. package/lib/module/features/engage/engageOutcome.js +108 -0
  484. package/lib/module/features/engage/engagePushTokenBridge.js +65 -0
  485. package/lib/module/features/engage/engageShakeBridge.js +56 -0
  486. package/lib/module/features/engage/engageSignals.js +34 -0
  487. package/lib/module/features/engage/engageStoreReviewBridge.js +43 -0
  488. package/lib/module/features/engage/engageTestSeenStore.js +92 -0
  489. package/lib/module/features/engage/engageThrottle.js +390 -0
  490. package/lib/module/features/engage/engageTriggerEngine.js +360 -0
  491. package/lib/module/features/engage/engageTypes.js +110 -0
  492. package/lib/module/features/engage/engageVariantResolver.js +135 -0
  493. package/lib/module/features/engage/engageWindowInsets.js +89 -0
  494. package/lib/module/features/engage/gestureTriggerDetector.js +80 -0
  495. package/lib/module/features/engage/transcription/TranscriptionProvider.js +46 -0
  496. package/lib/module/features/install-referrer/installReferrer.js +104 -0
  497. package/lib/module/features/journey/ScaleBunDebugRoot.js +1030 -0
  498. package/lib/module/features/journey/ScaleBunFlatList.js +32 -0
  499. package/lib/module/features/journey/ScaleBunImpression.js +105 -0
  500. package/lib/module/features/journey/ScaleBunScrollView.js +95 -0
  501. package/lib/module/features/journey/ScaleBunSectionList.js +32 -0
  502. package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
  503. package/lib/module/features/journey/calibrationContext.js +35 -0
  504. package/lib/module/features/journey/gestureBuffer.js +56 -0
  505. package/lib/module/features/journey/gestureDetector.js +169 -0
  506. package/lib/module/features/journey/impression.js +84 -0
  507. package/lib/module/features/journey/interactionProtocol.js +158 -0
  508. package/lib/module/features/journey/journeyManager.js +459 -0
  509. package/lib/module/features/journey/journeyTypes.js +49 -0
  510. package/lib/module/features/journey/nativeScroll.js +77 -0
  511. package/lib/module/features/journey/navDetector.js +154 -0
  512. package/lib/module/features/journey/screenshotHelper.js +129 -0
  513. package/lib/module/features/journey/scrollContext.js +142 -0
  514. package/lib/module/features/journey/targetGeometry.js +175 -0
  515. package/lib/module/features/journey/targetRegistry.js +44 -0
  516. package/lib/module/features/journey/touchTarget.js +241 -0
  517. package/lib/module/features/journey/uiState.js +176 -0
  518. package/lib/module/features/navigation/AutoScreenDetector.js +601 -0
  519. package/lib/module/features/network/NetworkFeature.js +209 -0
  520. package/lib/module/features/network/index.js +17 -0
  521. package/lib/module/features/network/thirdParty.js +142 -0
  522. package/lib/module/features/ota/OtaEventEmitter.js +123 -0
  523. package/lib/module/features/ota/OtaOrchestrator.js +1302 -0
  524. package/lib/module/features/ota/OtaTypes.js +2 -0
  525. package/lib/module/features/ota/deviceAttributes.js +43 -0
  526. package/lib/module/features/ota/environment.js +165 -0
  527. package/lib/module/features/ota/geoCountry.js +86 -0
  528. package/lib/module/features/ota/retry.js +87 -0
  529. package/lib/module/features/ota/signedDownload.js +174 -0
  530. package/lib/module/features/ota/useOtaUpdate.js +100 -0
  531. package/lib/module/features/performance/PerformanceFeature.js +643 -0
  532. package/lib/module/features/performance/collectors/AppLaunchCollector.js +115 -0
  533. package/lib/module/features/performance/collectors/AutoScreenLoadCollector.js +225 -0
  534. package/lib/module/features/performance/collectors/CustomTraceCollector.js +92 -0
  535. package/lib/module/features/performance/collectors/FrameMetricsCollector.js +177 -0
  536. package/lib/module/features/performance/collectors/JsStallCollector.js +158 -0
  537. package/lib/module/features/performance/collectors/NetworkPerfCollector.js +52 -0
  538. package/lib/module/features/performance/collectors/ScreenLoadCollector.js +71 -0
  539. package/lib/module/features/performance/collectors/UiHangCollector.js +71 -0
  540. package/lib/module/features/performance/config.js +51 -0
  541. package/lib/module/features/performance/getNativePerformanceModule.js +39 -0
  542. package/lib/module/features/performance/index.js +9 -0
  543. package/lib/module/features/performance/metrics/MetricsEngine.js +139 -0
  544. package/lib/module/features/performance/models.js +42 -0
  545. package/lib/module/features/performance/transport/PerformanceTransport.js +162 -0
  546. package/lib/module/features/profiler/ProfilerFeature.js +594 -0
  547. package/lib/module/features/profiler/config.js +143 -0
  548. package/lib/module/features/profiler/contracts.js +56 -0
  549. package/lib/module/features/profiler/index.js +11 -0
  550. package/lib/module/features/profiler/models.js +106 -0
  551. package/lib/module/features/profiler/transport/ProfilerTransport.js +87 -0
  552. package/lib/module/features/replay/bridge/adapters/bridgeAdapter.js +198 -0
  553. package/lib/module/features/replay/bridge/eventEmitter.js +122 -0
  554. package/lib/module/features/replay/bridge/nativeModule.js +89 -0
  555. package/lib/module/features/replay/bridge/nativeQuota.js +109 -0
  556. package/lib/module/features/replay/core/clock/clock.js +37 -0
  557. package/lib/module/features/replay/core/config/defaults.js +82 -0
  558. package/lib/module/features/replay/core/config/validator.js +103 -0
  559. package/lib/module/features/replay/core/env/environmentDetector.js +22 -0
  560. package/lib/module/features/replay/core/errors/safeCall.js +35 -0
  561. package/lib/module/features/replay/core/ids/sessionId.js +44 -0
  562. package/lib/module/features/replay/core/logger/logger.js +29 -0
  563. package/lib/module/features/replay/core/privacy/privacyTypes.js +27 -0
  564. package/lib/module/features/replay/core/queue/boundedQueue.js +64 -0
  565. package/lib/module/features/replay/integrations/alert/alertInstrumentation.js +252 -0
  566. package/lib/module/features/replay/integrations/logs/consoleIntegration.js +131 -0
  567. package/lib/module/features/replay/integrations/network/networkAdapter.js +70 -0
  568. package/lib/module/features/replay/integrations/react-navigation/navigationIntegration.js +82 -0
  569. package/lib/module/features/replay/integrations/touch/ReplayRoot.js +96 -0
  570. package/lib/module/features/replay/pipeline/batching/batchManager.js +113 -0
  571. package/lib/module/features/replay/pipeline/envelope/envelopeBuilder.js +43 -0
  572. package/lib/module/features/replay/pipeline/retry/retryPolicy.js +45 -0
  573. package/lib/module/features/replay/pipeline/transport/transportTypes.js +60 -0
  574. package/lib/module/features/replay/public/api.js +332 -0
  575. package/lib/module/features/replay/public/enums.js +85 -0
  576. package/lib/module/features/replay/public/types.js +2 -0
  577. package/lib/module/features/replay/replay/breadcrumbs/breadcrumbCollector.js +113 -0
  578. package/lib/module/features/replay/replay/frames/frameModels.js +33 -0
  579. package/lib/module/features/replay/replay/session/sessionOrchestrator.js +272 -0
  580. package/lib/module/features/replay/replay/timeline/timelineModels.js +108 -0
  581. package/lib/module/features/replay/transport/BackendReplayTransport.js +169 -0
  582. package/lib/module/features/replay/transport/CompositeReplayTransport.js +68 -0
  583. package/lib/module/features/replay/transport/DesktopReplayTransport.js +119 -0
  584. package/lib/module/features/replay/transport/ReplayTransport.js +2 -0
  585. package/lib/module/features/session/BackendSessionAdapter.js +1908 -0
  586. package/lib/module/features/session/DesktopSessionTransport.js +118 -0
  587. package/lib/module/features/session/JourneyEventPipeline.js +239 -0
  588. package/lib/module/features/session/ReplayCaptureManager.js +1248 -0
  589. package/lib/module/features/session/ScrollTracker.js +141 -0
  590. package/lib/module/features/session/SessionManager.js +1443 -0
  591. package/lib/module/features/session/SyncDecisionEngine.js +88 -0
  592. package/lib/module/features/session/frameLink.js +29 -0
  593. package/lib/module/features/session/frameScrollState.js +34 -0
  594. package/lib/module/features/session/index.js +35 -0
  595. package/lib/module/features/session/nativeCapture.js +45 -0
  596. package/lib/module/features/session/outboxFrameCapture.js +77 -0
  597. package/lib/module/features/session/sessionTypes.js +226 -0
  598. package/lib/module/features/session/viewportPlausibility.js +137 -0
  599. package/lib/module/features/skan/skanBridge.js +48 -0
  600. package/lib/module/features/skan/skanConversionManager.js +128 -0
  601. package/lib/module/index.js +80 -0
  602. package/lib/module/integrations/fetch/fetchInterceptor.js +169 -0
  603. package/lib/module/integrations/navigation/screenTracker.js +2 -0
  604. package/lib/module/integrations/network/bodyCapture.js +320 -0
  605. package/lib/module/integrations/xhr/xhrInterceptor.js +219 -0
  606. package/lib/module/metro/composeSourceMap.js +97 -0
  607. package/lib/module/metro/featureModules.js +123 -0
  608. package/lib/module/metro/index.js +218 -0
  609. package/lib/module/metro/optionalDependencyStub.js +14 -0
  610. package/lib/module/metro/optionalModules.js +25 -0
  611. package/lib/module/metro/serializerCompose.js +152 -0
  612. package/lib/module/pipeline/dispatcher/dispatcher.js +21 -0
  613. package/lib/module/pipeline/envelope/envelope.js +11 -0
  614. package/lib/module/pipeline/envelope/serializer.js +8 -0
  615. package/lib/module/pipeline/processors/dedupe.js +2 -0
  616. package/lib/module/pipeline/processors/enrich.js +10 -0
  617. package/lib/module/pipeline/processors/sampling.js +2 -0
  618. package/lib/module/pipeline/processors/sanitize.js +22 -0
  619. package/lib/module/pipeline/queue/flushQueue.js +102 -0
  620. package/lib/module/pipeline/queue/memoryQueue.js +24 -0
  621. package/lib/module/pipeline/queue/persistentQueue.js +336 -0
  622. package/lib/module/pipeline/queue/queuePolicy.js +2 -0
  623. package/lib/module/pipeline/scheduler/flushScheduler.js +38 -0
  624. package/lib/module/pipeline/scheduler/retryPolicy.js +9 -0
  625. package/lib/module/public/ScaleBunErrorBoundary.js +82 -0
  626. package/lib/module/public/ScaleBunFacade.js +2038 -0
  627. package/lib/module/public/ScaleBunProvider.js +54 -0
  628. package/lib/module/public/typedTracker.js +72 -0
  629. package/lib/module/public/types.js +2 -0
  630. package/lib/module/push/PushManager.js +263 -0
  631. package/lib/module/push/adapters/ManualAdapter.js +79 -0
  632. package/lib/module/push/adapters/NativeBridgeAdapter.js +124 -0
  633. package/lib/module/push/adapters/NoopAdapter.js +38 -0
  634. package/lib/module/push/adapters/RNFirebaseMessagingAdapter.js +241 -0
  635. package/lib/module/push/adapters/loadMessaging.js +23 -0
  636. package/lib/module/push/adapters/loadNotifee.js +30 -0
  637. package/lib/module/push/adapters/selectAdapter.js +147 -0
  638. package/lib/module/push/detect/capabilities.js +130 -0
  639. package/lib/module/push/index.js +9 -0
  640. package/lib/module/push/native/nativeNotifications.js +155 -0
  641. package/lib/module/push/types.js +2 -0
  642. package/lib/module/specs/NativeReplaySdk.js +24 -0
  643. package/lib/module/specs/NativeScaleBunCrash.js +26 -0
  644. package/lib/module/specs/NativeScaleBunInstallReferrer.js +20 -0
  645. package/lib/module/specs/NativeScaleBunOta.js +28 -0
  646. package/lib/module/specs/NativeScaleBunPerformance.js +21 -0
  647. package/lib/module/specs/NativeScaleBunProfiler.js +22 -0
  648. package/lib/module/specs/NativeScaleBunStorage.js +36 -0
  649. package/lib/module/storage/StorageBackend.js +231 -0
  650. package/lib/module/storage/db/sqlite.js +2 -0
  651. package/lib/module/storage/files/fileStore.js +2 -0
  652. package/lib/module/transport/auth/sdkSession.js +21 -0
  653. package/lib/module/transport/backoff/exponentialJitter.js +10 -0
  654. package/lib/module/transport/http/endpoints.js +40 -0
  655. package/lib/module/transport/http/httpClient.js +64 -0
  656. package/lib/module/transport/rateLimit/retryAfter.js +2 -0
  657. package/lib/module/transport/signing/requestSigner.js +2 -0
  658. package/lib/module/vendor/protocol/index.js +312 -0
  659. package/lib/module/vendor/protocol/internal/validation.js +423 -0
  660. package/lib/module/vendor/sdk-contracts/bridge.js +2 -0
  661. package/lib/module/vendor/sdk-contracts/config.js +2 -0
  662. package/lib/module/vendor/sdk-contracts/events.js +2 -0
  663. package/lib/module/vendor/sdk-contracts/features.js +2 -0
  664. package/lib/module/vendor/sdk-contracts/index.js +2 -0
  665. package/lib/module/vendor/sdk-contracts/platform.js +2 -0
  666. package/lib/module/vendor/sdk-contracts/session-boundary.js +2 -0
  667. package/lib/module/vendor/sdk-contracts/session.js +2 -0
  668. package/lib/module/vendor/vendor.manifest +10 -0
  669. package/lib/typescript/analytics/EventTracker.d.ts +197 -0
  670. package/lib/typescript/analytics/automaticEvents.d.ts +30 -0
  671. package/lib/typescript/analytics/batching.d.ts +33 -0
  672. package/lib/typescript/analytics/eventLane.d.ts +22 -0
  673. package/lib/typescript/analytics/revenue.d.ts +94 -0
  674. package/lib/typescript/analytics/subscription.d.ts +35 -0
  675. package/lib/typescript/bootstrap/FeatureRegistry.d.ts +36 -0
  676. package/lib/typescript/bootstrap/SDKBootstrapper.d.ts +82 -0
  677. package/lib/typescript/bucketing/fnv1a32.d.ts +20 -0
  678. package/lib/typescript/compat/codepush.d.ts +76 -0
  679. package/lib/typescript/config/ConfigManager.d.ts +82 -0
  680. package/lib/typescript/config/configTypes.d.ts +73 -0
  681. package/lib/typescript/config/otaPolicyState.d.ts +55 -0
  682. package/lib/typescript/config/quotaState.d.ts +155 -0
  683. package/lib/typescript/core/architecture.d.ts +29 -0
  684. package/lib/typescript/core/clock/now.d.ts +5 -0
  685. package/lib/typescript/core/config/defaults.d.ts +1 -0
  686. package/lib/typescript/core/config/schema.d.ts +107 -0
  687. package/lib/typescript/core/constants/endpoints.d.ts +13 -0
  688. package/lib/typescript/core/constants/protocol.d.ts +23 -0
  689. package/lib/typescript/core/constants/timings.d.ts +22 -0
  690. package/lib/typescript/core/constants/version.d.ts +12 -0
  691. package/lib/typescript/core/context/app.d.ts +1 -0
  692. package/lib/typescript/core/context/device.d.ts +61 -0
  693. package/lib/typescript/core/context/session.d.ts +1 -0
  694. package/lib/typescript/core/context/user.d.ts +24 -0
  695. package/lib/typescript/core/contracts/IFeature.d.ts +44 -0
  696. package/lib/typescript/core/di/container.d.ts +9 -0
  697. package/lib/typescript/core/encoding/base64.d.ts +13 -0
  698. package/lib/typescript/core/id/deviceId.d.ts +9 -0
  699. package/lib/typescript/core/id/installationId.d.ts +20 -0
  700. package/lib/typescript/core/id/sessionId.d.ts +1 -0
  701. package/lib/typescript/core/lifecycle/appLifecycle.d.ts +14 -0
  702. package/lib/typescript/core/lifecycle/crashSafe.d.ts +7 -0
  703. package/lib/typescript/core/logger/errorClassification.d.ts +46 -0
  704. package/lib/typescript/core/logger/internalLogger.d.ts +24 -0
  705. package/lib/typescript/core/logger/levels.d.ts +1 -0
  706. package/lib/typescript/crypto/hmacSha256.d.ts +10 -0
  707. package/lib/typescript/debug/bootstrap.d.ts +148 -0
  708. package/lib/typescript/debug/commands.d.ts +77 -0
  709. package/lib/typescript/debug/configBuilder.d.ts +13 -0
  710. package/lib/typescript/debug/exportBridge.d.ts +27 -0
  711. package/lib/typescript/debug/hostResolver.d.ts +14 -0
  712. package/lib/typescript/debug/index.d.ts +12 -0
  713. package/lib/typescript/debug/metrics.d.ts +2 -0
  714. package/lib/typescript/debug/perf.d.ts +43 -0
  715. package/lib/typescript/debug/perfWiring.d.ts +33 -0
  716. package/lib/typescript/debug/protocol.d.ts +15 -0
  717. package/lib/typescript/debug/redaction.d.ts +69 -0
  718. package/lib/typescript/debug/replayWiring.d.ts +25 -0
  719. package/lib/typescript/debug/screenTracking.d.ts +16 -0
  720. package/lib/typescript/debug/sessionWiring.d.ts +16 -0
  721. package/lib/typescript/debug/stream.d.ts +77 -0
  722. package/lib/typescript/debug/transport.d.ts +110 -0
  723. package/lib/typescript/features/bugreport/BugReportFeature.d.ts +59 -0
  724. package/lib/typescript/features/bugreport/buildPayload.d.ts +51 -0
  725. package/lib/typescript/features/bugreport/index.d.ts +1 -0
  726. package/lib/typescript/features/crash/CrashFeature.d.ts +21 -0
  727. package/lib/typescript/features/crash/CrashReporter.d.ts +107 -0
  728. package/lib/typescript/features/crash/NativeCrashFeature.d.ts +17 -0
  729. package/lib/typescript/features/crash/capture.d.ts +22 -0
  730. package/lib/typescript/features/crash/globalHandler.d.ts +14 -0
  731. package/lib/typescript/features/crash/index.d.ts +2 -0
  732. package/lib/typescript/features/crash/nativeCrashBridge.d.ts +43 -0
  733. package/lib/typescript/features/crash/rejectionHandler.d.ts +65 -0
  734. package/lib/typescript/features/crash/scrubCrash.d.ts +35 -0
  735. package/lib/typescript/features/engage/EngageAnchor.d.ts +36 -0
  736. package/lib/typescript/features/engage/EngageArchetypeRenderers.d.ts +66 -0
  737. package/lib/typescript/features/engage/EngageInAppView.d.ts +164 -0
  738. package/lib/typescript/features/engage/EngageInlinePlacement.d.ts +42 -0
  739. package/lib/typescript/features/engage/EngagePromptProvider.d.ts +233 -0
  740. package/lib/typescript/features/engage/EngagePromptView.d.ts +97 -0
  741. package/lib/typescript/features/engage/EngageTransport.d.ts +158 -0
  742. package/lib/typescript/features/engage/EngageVariantContent.d.ts +12 -0
  743. package/lib/typescript/features/engage/attachmentCapture.d.ts +17 -0
  744. package/lib/typescript/features/engage/captureVoiceAttachment.d.ts +31 -0
  745. package/lib/typescript/features/engage/engageCoachmarkTour.d.ts +18 -0
  746. package/lib/typescript/features/engage/engageGameLogic.d.ts +34 -0
  747. package/lib/typescript/features/engage/engageMediaSizing.d.ts +23 -0
  748. package/lib/typescript/features/engage/engageMultiStepSheet.d.ts +25 -0
  749. package/lib/typescript/features/engage/engageOutcome.d.ts +38 -0
  750. package/lib/typescript/features/engage/engagePushTokenBridge.d.ts +30 -0
  751. package/lib/typescript/features/engage/engageShakeBridge.d.ts +20 -0
  752. package/lib/typescript/features/engage/engageSignals.d.ts +13 -0
  753. package/lib/typescript/features/engage/engageStoreReviewBridge.d.ts +23 -0
  754. package/lib/typescript/features/engage/engageTestSeenStore.d.ts +46 -0
  755. package/lib/typescript/features/engage/engageThrottle.d.ts +180 -0
  756. package/lib/typescript/features/engage/engageTriggerEngine.d.ts +16 -0
  757. package/lib/typescript/features/engage/engageTypes.d.ts +576 -0
  758. package/lib/typescript/features/engage/engageVariantResolver.d.ts +46 -0
  759. package/lib/typescript/features/engage/engageWindowInsets.d.ts +25 -0
  760. package/lib/typescript/features/engage/gestureTriggerDetector.d.ts +20 -0
  761. package/lib/typescript/features/engage/transcription/TranscriptionProvider.d.ts +51 -0
  762. package/lib/typescript/features/install-referrer/installReferrer.d.ts +39 -0
  763. package/lib/typescript/features/journey/ScaleBunDebugRoot.d.ts +49 -0
  764. package/lib/typescript/features/journey/ScaleBunFlatList.d.ts +20 -0
  765. package/lib/typescript/features/journey/ScaleBunImpression.d.ts +30 -0
  766. package/lib/typescript/features/journey/ScaleBunScrollView.d.ts +25 -0
  767. package/lib/typescript/features/journey/ScaleBunSectionList.d.ts +20 -0
  768. package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
  769. package/lib/typescript/features/journey/calibrationContext.d.ts +26 -0
  770. package/lib/typescript/features/journey/gestureBuffer.d.ts +27 -0
  771. package/lib/typescript/features/journey/gestureDetector.d.ts +40 -0
  772. package/lib/typescript/features/journey/impression.d.ts +48 -0
  773. package/lib/typescript/features/journey/interactionProtocol.d.ts +159 -0
  774. package/lib/typescript/features/journey/journeyManager.d.ts +62 -0
  775. package/lib/typescript/features/journey/journeyTypes.d.ts +161 -0
  776. package/lib/typescript/features/journey/nativeScroll.d.ts +47 -0
  777. package/lib/typescript/features/journey/navDetector.d.ts +42 -0
  778. package/lib/typescript/features/journey/screenshotHelper.d.ts +33 -0
  779. package/lib/typescript/features/journey/scrollContext.d.ts +49 -0
  780. package/lib/typescript/features/journey/targetGeometry.d.ts +126 -0
  781. package/lib/typescript/features/journey/targetRegistry.d.ts +28 -0
  782. package/lib/typescript/features/journey/touchTarget.d.ts +174 -0
  783. package/lib/typescript/features/journey/uiState.d.ts +93 -0
  784. package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +196 -0
  785. package/lib/typescript/features/network/NetworkFeature.d.ts +43 -0
  786. package/lib/typescript/features/network/index.d.ts +46 -0
  787. package/lib/typescript/features/network/thirdParty.d.ts +95 -0
  788. package/lib/typescript/features/ota/OtaEventEmitter.d.ts +93 -0
  789. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +287 -0
  790. package/lib/typescript/features/ota/OtaTypes.d.ts +193 -0
  791. package/lib/typescript/features/ota/deviceAttributes.d.ts +18 -0
  792. package/lib/typescript/features/ota/environment.d.ts +99 -0
  793. package/lib/typescript/features/ota/geoCountry.d.ts +14 -0
  794. package/lib/typescript/features/ota/retry.d.ts +53 -0
  795. package/lib/typescript/features/ota/signedDownload.d.ts +81 -0
  796. package/lib/typescript/features/ota/useOtaUpdate.d.ts +72 -0
  797. package/lib/typescript/features/performance/PerformanceFeature.d.ts +132 -0
  798. package/lib/typescript/features/performance/collectors/AppLaunchCollector.d.ts +27 -0
  799. package/lib/typescript/features/performance/collectors/AutoScreenLoadCollector.d.ts +70 -0
  800. package/lib/typescript/features/performance/collectors/CustomTraceCollector.d.ts +35 -0
  801. package/lib/typescript/features/performance/collectors/FrameMetricsCollector.d.ts +74 -0
  802. package/lib/typescript/features/performance/collectors/JsStallCollector.d.ts +45 -0
  803. package/lib/typescript/features/performance/collectors/NetworkPerfCollector.d.ts +23 -0
  804. package/lib/typescript/features/performance/collectors/ScreenLoadCollector.d.ts +30 -0
  805. package/lib/typescript/features/performance/collectors/UiHangCollector.d.ts +21 -0
  806. package/lib/typescript/features/performance/config.d.ts +73 -0
  807. package/lib/typescript/features/performance/getNativePerformanceModule.d.ts +2 -0
  808. package/lib/typescript/features/performance/index.d.ts +10 -0
  809. package/lib/typescript/features/performance/metrics/MetricsEngine.d.ts +69 -0
  810. package/lib/typescript/features/performance/models.d.ts +169 -0
  811. package/lib/typescript/features/performance/transport/PerformanceTransport.d.ts +40 -0
  812. package/lib/typescript/features/profiler/ProfilerFeature.d.ts +92 -0
  813. package/lib/typescript/features/profiler/config.d.ts +85 -0
  814. package/lib/typescript/features/profiler/contracts.d.ts +142 -0
  815. package/lib/typescript/features/profiler/index.d.ts +13 -0
  816. package/lib/typescript/features/profiler/models.d.ts +332 -0
  817. package/lib/typescript/features/profiler/transport/ProfilerTransport.d.ts +30 -0
  818. package/lib/typescript/features/replay/bridge/adapters/bridgeAdapter.d.ts +52 -0
  819. package/lib/typescript/features/replay/bridge/eventEmitter.d.ts +100 -0
  820. package/lib/typescript/features/replay/bridge/nativeModule.d.ts +166 -0
  821. package/lib/typescript/features/replay/bridge/nativeQuota.d.ts +26 -0
  822. package/lib/typescript/features/replay/core/clock/clock.d.ts +16 -0
  823. package/lib/typescript/features/replay/core/config/defaults.d.ts +28 -0
  824. package/lib/typescript/features/replay/core/config/validator.d.ts +13 -0
  825. package/lib/typescript/features/replay/core/env/environmentDetector.d.ts +14 -0
  826. package/lib/typescript/features/replay/core/errors/safeCall.d.ts +12 -0
  827. package/lib/typescript/features/replay/core/ids/sessionId.d.ts +15 -0
  828. package/lib/typescript/features/replay/core/logger/logger.d.ts +15 -0
  829. package/lib/typescript/features/replay/core/privacy/privacyTypes.d.ts +24 -0
  830. package/lib/typescript/features/replay/core/queue/boundedQueue.d.ts +29 -0
  831. package/lib/typescript/features/replay/integrations/alert/alertInstrumentation.d.ts +48 -0
  832. package/lib/typescript/features/replay/integrations/logs/consoleIntegration.d.ts +18 -0
  833. package/lib/typescript/features/replay/integrations/network/networkAdapter.d.ts +31 -0
  834. package/lib/typescript/features/replay/integrations/react-navigation/navigationIntegration.d.ts +25 -0
  835. package/lib/typescript/features/replay/integrations/touch/ReplayRoot.d.ts +26 -0
  836. package/lib/typescript/features/replay/pipeline/batching/batchManager.d.ts +47 -0
  837. package/lib/typescript/features/replay/pipeline/envelope/envelopeBuilder.d.ts +12 -0
  838. package/lib/typescript/features/replay/pipeline/retry/retryPolicy.d.ts +17 -0
  839. package/lib/typescript/features/replay/pipeline/transport/transportTypes.d.ts +40 -0
  840. package/lib/typescript/features/replay/public/api.d.ts +83 -0
  841. package/lib/typescript/features/replay/public/enums.d.ts +73 -0
  842. package/lib/typescript/features/replay/public/types.d.ts +249 -0
  843. package/lib/typescript/features/replay/replay/breadcrumbs/breadcrumbCollector.d.ts +26 -0
  844. package/lib/typescript/features/replay/replay/frames/frameModels.d.ts +23 -0
  845. package/lib/typescript/features/replay/replay/session/sessionOrchestrator.d.ts +53 -0
  846. package/lib/typescript/features/replay/replay/timeline/timelineModels.d.ts +29 -0
  847. package/lib/typescript/features/replay/transport/BackendReplayTransport.d.ts +45 -0
  848. package/lib/typescript/features/replay/transport/CompositeReplayTransport.d.ts +22 -0
  849. package/lib/typescript/features/replay/transport/DesktopReplayTransport.d.ts +35 -0
  850. package/lib/typescript/features/replay/transport/ReplayTransport.d.ts +72 -0
  851. package/lib/typescript/features/session/BackendSessionAdapter.d.ts +528 -0
  852. package/lib/typescript/features/session/DesktopSessionTransport.d.ts +28 -0
  853. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +74 -0
  854. package/lib/typescript/features/session/ReplayCaptureManager.d.ts +391 -0
  855. package/lib/typescript/features/session/ScrollTracker.d.ts +48 -0
  856. package/lib/typescript/features/session/SessionManager.d.ts +414 -0
  857. package/lib/typescript/features/session/SyncDecisionEngine.d.ts +33 -0
  858. package/lib/typescript/features/session/frameLink.d.ts +27 -0
  859. package/lib/typescript/features/session/frameScrollState.d.ts +27 -0
  860. package/lib/typescript/features/session/index.d.ts +23 -0
  861. package/lib/typescript/features/session/nativeCapture.d.ts +18 -0
  862. package/lib/typescript/features/session/outboxFrameCapture.d.ts +20 -0
  863. package/lib/typescript/features/session/sessionTypes.d.ts +362 -0
  864. package/lib/typescript/features/session/viewportPlausibility.d.ts +72 -0
  865. package/lib/typescript/features/skan/skanBridge.d.ts +18 -0
  866. package/lib/typescript/features/skan/skanConversionManager.d.ts +69 -0
  867. package/lib/typescript/index.d.ts +59 -0
  868. package/lib/typescript/integrations/fetch/fetchInterceptor.d.ts +41 -0
  869. package/lib/typescript/integrations/navigation/screenTracker.d.ts +1 -0
  870. package/lib/typescript/integrations/network/bodyCapture.d.ts +101 -0
  871. package/lib/typescript/integrations/xhr/xhrInterceptor.d.ts +46 -0
  872. package/lib/typescript/metro/composeSourceMap.d.ts +26 -0
  873. package/lib/typescript/metro/featureModules.d.ts +49 -0
  874. package/lib/typescript/metro/index.d.ts +99 -0
  875. package/lib/typescript/metro/optionalDependencyStub.d.ts +14 -0
  876. package/lib/typescript/metro/optionalModules.d.ts +20 -0
  877. package/lib/typescript/metro/serializerCompose.d.ts +46 -0
  878. package/lib/typescript/pipeline/dispatcher/dispatcher.d.ts +7 -0
  879. package/lib/typescript/pipeline/envelope/envelope.d.ts +16 -0
  880. package/lib/typescript/pipeline/envelope/serializer.d.ts +7 -0
  881. package/lib/typescript/pipeline/processors/dedupe.d.ts +1 -0
  882. package/lib/typescript/pipeline/processors/enrich.d.ts +3 -0
  883. package/lib/typescript/pipeline/processors/sampling.d.ts +1 -0
  884. package/lib/typescript/pipeline/processors/sanitize.d.ts +3 -0
  885. package/lib/typescript/pipeline/queue/flushQueue.d.ts +12 -0
  886. package/lib/typescript/pipeline/queue/memoryQueue.d.ts +12 -0
  887. package/lib/typescript/pipeline/queue/persistentQueue.d.ts +120 -0
  888. package/lib/typescript/pipeline/queue/queuePolicy.d.ts +1 -0
  889. package/lib/typescript/pipeline/scheduler/flushScheduler.d.ts +9 -0
  890. package/lib/typescript/pipeline/scheduler/retryPolicy.d.ts +5 -0
  891. package/lib/typescript/public/ScaleBunErrorBoundary.d.ts +44 -0
  892. package/lib/typescript/public/ScaleBunFacade.d.ts +800 -0
  893. package/lib/typescript/public/ScaleBunProvider.d.ts +23 -0
  894. package/lib/typescript/public/typedTracker.d.ts +88 -0
  895. package/lib/typescript/public/types.d.ts +316 -0
  896. package/lib/typescript/push/PushManager.d.ts +69 -0
  897. package/lib/typescript/push/adapters/ManualAdapter.d.ts +40 -0
  898. package/lib/typescript/push/adapters/NativeBridgeAdapter.d.ts +37 -0
  899. package/lib/typescript/push/adapters/NoopAdapter.d.ts +21 -0
  900. package/lib/typescript/push/adapters/RNFirebaseMessagingAdapter.d.ts +39 -0
  901. package/lib/typescript/push/adapters/loadMessaging.d.ts +17 -0
  902. package/lib/typescript/push/adapters/loadNotifee.d.ts +15 -0
  903. package/lib/typescript/push/adapters/selectAdapter.d.ts +20 -0
  904. package/lib/typescript/push/detect/capabilities.d.ts +15 -0
  905. package/lib/typescript/push/index.d.ts +10 -0
  906. package/lib/typescript/push/native/nativeNotifications.d.ts +65 -0
  907. package/lib/typescript/push/types.d.ts +143 -0
  908. package/lib/typescript/specs/NativeReplaySdk.d.ts +69 -0
  909. package/lib/typescript/specs/NativeScaleBunCrash.d.ts +58 -0
  910. package/lib/typescript/specs/NativeScaleBunInstallReferrer.d.ts +28 -0
  911. package/lib/typescript/specs/NativeScaleBunOta.d.ts +141 -0
  912. package/lib/typescript/specs/NativeScaleBunPerformance.d.ts +31 -0
  913. package/lib/typescript/specs/NativeScaleBunProfiler.d.ts +38 -0
  914. package/lib/typescript/specs/NativeScaleBunStorage.d.ts +55 -0
  915. package/lib/typescript/storage/StorageBackend.d.ts +131 -0
  916. package/lib/typescript/storage/db/sqlite.d.ts +1 -0
  917. package/lib/typescript/storage/files/fileStore.d.ts +1 -0
  918. package/lib/typescript/transport/auth/sdkSession.d.ts +10 -0
  919. package/lib/typescript/transport/backoff/exponentialJitter.d.ts +5 -0
  920. package/lib/typescript/transport/http/endpoints.d.ts +26 -0
  921. package/lib/typescript/transport/http/httpClient.d.ts +23 -0
  922. package/lib/typescript/transport/rateLimit/retryAfter.d.ts +1 -0
  923. package/lib/typescript/transport/signing/requestSigner.d.ts +1 -0
  924. package/lib/typescript/vendor/protocol/index.d.ts +345 -0
  925. package/lib/typescript/vendor/protocol/internal/validation.d.ts +114 -0
  926. package/lib/typescript/vendor/sdk-contracts/bridge.d.ts +104 -0
  927. package/lib/typescript/vendor/sdk-contracts/config.d.ts +156 -0
  928. package/lib/typescript/vendor/sdk-contracts/events.d.ts +42 -0
  929. package/lib/typescript/vendor/sdk-contracts/features.d.ts +38 -0
  930. package/lib/typescript/vendor/sdk-contracts/index.d.ts +15 -0
  931. package/lib/typescript/vendor/sdk-contracts/platform.d.ts +35 -0
  932. package/lib/typescript/vendor/sdk-contracts/session-boundary.d.ts +42 -0
  933. package/lib/typescript/vendor/sdk-contracts/session.d.ts +157 -0
  934. package/package.json +153 -0
  935. package/react-native.config.js +24 -0
  936. package/scalebun-react-native.podspec +44 -0
  937. package/src/analytics/EventTracker.ts +654 -0
  938. package/src/analytics/automaticEvents.ts +109 -0
  939. package/src/analytics/batching.ts +54 -0
  940. package/src/analytics/eventLane.ts +48 -0
  941. package/src/analytics/revenue.ts +143 -0
  942. package/src/analytics/subscription.ts +145 -0
  943. package/src/bootstrap/FeatureRegistry.ts +81 -0
  944. package/src/bootstrap/SDKBootstrapper.ts +769 -0
  945. package/src/bucketing/fnv1a32.ts +40 -0
  946. package/src/compat/codepush.ts +189 -0
  947. package/src/config/ConfigManager.ts +233 -0
  948. package/src/config/configTypes.ts +62 -0
  949. package/src/config/otaPolicyState.ts +79 -0
  950. package/src/config/quotaState.ts +484 -0
  951. package/src/core/architecture.ts +39 -0
  952. package/src/core/clock/now.ts +6 -0
  953. package/src/core/config/defaults.ts +0 -0
  954. package/src/core/config/schema.ts +455 -0
  955. package/src/core/constants/endpoints.ts +12 -0
  956. package/src/core/constants/protocol.ts +24 -0
  957. package/src/core/constants/timings.ts +25 -0
  958. package/src/core/constants/version.ts +11 -0
  959. package/src/core/context/app.ts +0 -0
  960. package/src/core/context/device.ts +169 -0
  961. package/src/core/context/session.ts +0 -0
  962. package/src/core/context/user.ts +26 -0
  963. package/src/core/contracts/IFeature.ts +53 -0
  964. package/src/core/di/container.ts +26 -0
  965. package/src/core/encoding/base64.ts +46 -0
  966. package/src/core/id/deviceId.ts +51 -0
  967. package/src/core/id/installationId.ts +52 -0
  968. package/src/core/id/sessionId.ts +0 -0
  969. package/src/core/lifecycle/appLifecycle.ts +33 -0
  970. package/src/core/lifecycle/crashSafe.ts +29 -0
  971. package/src/core/logger/errorClassification.ts +108 -0
  972. package/src/core/logger/internalLogger.ts +65 -0
  973. package/src/core/logger/levels.ts +0 -0
  974. package/src/crypto/hmacSha256.ts +106 -0
  975. package/src/debug/bootstrap.ts +436 -0
  976. package/src/debug/commands.ts +597 -0
  977. package/src/debug/configBuilder.ts +56 -0
  978. package/src/debug/exportBridge.ts +155 -0
  979. package/src/debug/hostResolver.ts +34 -0
  980. package/src/debug/index.ts +25 -0
  981. package/src/debug/metrics.ts +29 -0
  982. package/src/debug/perf.ts +212 -0
  983. package/src/debug/perfWiring.ts +119 -0
  984. package/src/debug/protocol.ts +58 -0
  985. package/src/debug/redaction.ts +483 -0
  986. package/src/debug/replayWiring.ts +148 -0
  987. package/src/debug/screenTracking.ts +55 -0
  988. package/src/debug/sessionWiring.ts +59 -0
  989. package/src/debug/stream.ts +561 -0
  990. package/src/debug/transport.ts +440 -0
  991. package/src/features/bugreport/BugReportFeature.ts +128 -0
  992. package/src/features/bugreport/buildPayload.ts +116 -0
  993. package/src/features/bugreport/index.ts +0 -0
  994. package/src/features/crash/CrashFeature.ts +87 -0
  995. package/src/features/crash/CrashReporter.ts +189 -0
  996. package/src/features/crash/NativeCrashFeature.ts +42 -0
  997. package/src/features/crash/capture.ts +48 -0
  998. package/src/features/crash/globalHandler.ts +39 -0
  999. package/src/features/crash/index.ts +8 -0
  1000. package/src/features/crash/nativeCrashBridge.ts +352 -0
  1001. package/src/features/crash/rejectionHandler.ts +196 -0
  1002. package/src/features/crash/scrubCrash.ts +175 -0
  1003. package/src/features/engage/EngageAnchor.tsx +159 -0
  1004. package/src/features/engage/EngageArchetypeRenderers.tsx +3705 -0
  1005. package/src/features/engage/EngageInAppView.tsx +2678 -0
  1006. package/src/features/engage/EngageInlinePlacement.tsx +169 -0
  1007. package/src/features/engage/EngagePromptProvider.tsx +1318 -0
  1008. package/src/features/engage/EngagePromptView.tsx +1400 -0
  1009. package/src/features/engage/EngageTransport.ts +668 -0
  1010. package/src/features/engage/EngageVariantContent.tsx +1483 -0
  1011. package/src/features/engage/__test-support__/reactNativeStub.js +40 -0
  1012. package/src/features/engage/attachmentCapture.ts +45 -0
  1013. package/src/features/engage/captureVoiceAttachment.ts +81 -0
  1014. package/src/features/engage/engageCoachmarkTour.ts +120 -0
  1015. package/src/features/engage/engageGameLogic.ts +347 -0
  1016. package/src/features/engage/engageMediaSizing.ts +76 -0
  1017. package/src/features/engage/engageMultiStepSheet.ts +53 -0
  1018. package/src/features/engage/engageOutcome.ts +157 -0
  1019. package/src/features/engage/engagePushTokenBridge.ts +84 -0
  1020. package/src/features/engage/engageShakeBridge.ts +67 -0
  1021. package/src/features/engage/engageSignals.ts +40 -0
  1022. package/src/features/engage/engageStoreReviewBridge.ts +48 -0
  1023. package/src/features/engage/engageTestSeenStore.ts +95 -0
  1024. package/src/features/engage/engageThrottle.ts +478 -0
  1025. package/src/features/engage/engageTriggerEngine.ts +350 -0
  1026. package/src/features/engage/engageTypes.ts +712 -0
  1027. package/src/features/engage/engageVariantResolver.ts +364 -0
  1028. package/src/features/engage/engageWindowInsets.ts +108 -0
  1029. package/src/features/engage/gestureTriggerDetector.ts +83 -0
  1030. package/src/features/engage/transcription/TranscriptionProvider.ts +68 -0
  1031. package/src/features/install-referrer/installReferrer.ts +112 -0
  1032. package/src/features/journey/ScaleBunDebugRoot.tsx +1045 -0
  1033. package/src/features/journey/ScaleBunFlatList.tsx +39 -0
  1034. package/src/features/journey/ScaleBunImpression.tsx +109 -0
  1035. package/src/features/journey/ScaleBunScrollView.tsx +101 -0
  1036. package/src/features/journey/ScaleBunSectionList.tsx +39 -0
  1037. package/src/features/journey/autoInstrumentScroll.ts +155 -0
  1038. package/src/features/journey/calibrationContext.ts +44 -0
  1039. package/src/features/journey/gestureBuffer.ts +59 -0
  1040. package/src/features/journey/gestureDetector.ts +162 -0
  1041. package/src/features/journey/impression.ts +92 -0
  1042. package/src/features/journey/interactionProtocol.ts +243 -0
  1043. package/src/features/journey/journeyManager.ts +469 -0
  1044. package/src/features/journey/journeyTypes.ts +215 -0
  1045. package/src/features/journey/nativeScroll.ts +94 -0
  1046. package/src/features/journey/navDetector.ts +149 -0
  1047. package/src/features/journey/screenshotHelper.ts +137 -0
  1048. package/src/features/journey/scrollContext.ts +146 -0
  1049. package/src/features/journey/targetGeometry.ts +197 -0
  1050. package/src/features/journey/targetRegistry.ts +43 -0
  1051. package/src/features/journey/touchTarget.ts +339 -0
  1052. package/src/features/journey/uiState.ts +182 -0
  1053. package/src/features/navigation/AutoScreenDetector.ts +617 -0
  1054. package/src/features/network/NetworkFeature.ts +232 -0
  1055. package/src/features/network/index.ts +59 -0
  1056. package/src/features/network/thirdParty.ts +160 -0
  1057. package/src/features/ota/OtaEventEmitter.ts +187 -0
  1058. package/src/features/ota/OtaOrchestrator.ts +1513 -0
  1059. package/src/features/ota/OtaTypes.ts +230 -0
  1060. package/src/features/ota/deviceAttributes.ts +46 -0
  1061. package/src/features/ota/environment.ts +199 -0
  1062. package/src/features/ota/geoCountry.ts +85 -0
  1063. package/src/features/ota/retry.ts +123 -0
  1064. package/src/features/ota/signedDownload.ts +202 -0
  1065. package/src/features/ota/useOtaUpdate.ts +161 -0
  1066. package/src/features/performance/PerformanceFeature.ts +729 -0
  1067. package/src/features/performance/collectors/AppLaunchCollector.ts +131 -0
  1068. package/src/features/performance/collectors/AutoScreenLoadCollector.ts +258 -0
  1069. package/src/features/performance/collectors/CustomTraceCollector.ts +116 -0
  1070. package/src/features/performance/collectors/FrameMetricsCollector.ts +210 -0
  1071. package/src/features/performance/collectors/JsStallCollector.ts +184 -0
  1072. package/src/features/performance/collectors/NetworkPerfCollector.ts +69 -0
  1073. package/src/features/performance/collectors/ScreenLoadCollector.ts +95 -0
  1074. package/src/features/performance/collectors/UiHangCollector.ts +98 -0
  1075. package/src/features/performance/config.ts +136 -0
  1076. package/src/features/performance/getNativePerformanceModule.ts +38 -0
  1077. package/src/features/performance/index.ts +28 -0
  1078. package/src/features/performance/metrics/MetricsEngine.ts +152 -0
  1079. package/src/features/performance/models.ts +235 -0
  1080. package/src/features/performance/transport/PerformanceTransport.ts +161 -0
  1081. package/src/features/profiler/ProfilerFeature.ts +661 -0
  1082. package/src/features/profiler/config.ts +230 -0
  1083. package/src/features/profiler/contracts.ts +176 -0
  1084. package/src/features/profiler/index.ts +45 -0
  1085. package/src/features/profiler/models.ts +430 -0
  1086. package/src/features/profiler/transport/ProfilerTransport.ts +93 -0
  1087. package/src/features/replay/bridge/adapters/bridgeAdapter.ts +235 -0
  1088. package/src/features/replay/bridge/eventEmitter.ts +176 -0
  1089. package/src/features/replay/bridge/nativeModule.ts +252 -0
  1090. package/src/features/replay/bridge/nativeQuota.ts +111 -0
  1091. package/src/features/replay/core/clock/clock.ts +38 -0
  1092. package/src/features/replay/core/config/defaults.ts +66 -0
  1093. package/src/features/replay/core/config/validator.ts +113 -0
  1094. package/src/features/replay/core/env/environmentDetector.ts +21 -0
  1095. package/src/features/replay/core/errors/safeCall.ts +34 -0
  1096. package/src/features/replay/core/ids/sessionId.ts +44 -0
  1097. package/src/features/replay/core/logger/logger.ts +36 -0
  1098. package/src/features/replay/core/privacy/privacyTypes.ts +41 -0
  1099. package/src/features/replay/core/queue/boundedQueue.ts +71 -0
  1100. package/src/features/replay/integrations/alert/alertInstrumentation.ts +277 -0
  1101. package/src/features/replay/integrations/logs/consoleIntegration.ts +136 -0
  1102. package/src/features/replay/integrations/network/networkAdapter.ts +73 -0
  1103. package/src/features/replay/integrations/react-navigation/navigationIntegration.ts +89 -0
  1104. package/src/features/replay/integrations/touch/ReplayRoot.tsx +114 -0
  1105. package/src/features/replay/pipeline/batching/batchManager.ts +133 -0
  1106. package/src/features/replay/pipeline/envelope/envelopeBuilder.ts +66 -0
  1107. package/src/features/replay/pipeline/retry/retryPolicy.ts +62 -0
  1108. package/src/features/replay/pipeline/transport/transportTypes.ts +85 -0
  1109. package/src/features/replay/public/api.ts +364 -0
  1110. package/src/features/replay/public/enums.ts +78 -0
  1111. package/src/features/replay/public/types.ts +297 -0
  1112. package/src/features/replay/replay/breadcrumbs/breadcrumbCollector.ts +115 -0
  1113. package/src/features/replay/replay/frames/frameModels.ts +45 -0
  1114. package/src/features/replay/replay/session/sessionOrchestrator.ts +286 -0
  1115. package/src/features/replay/replay/timeline/timelineModels.ts +129 -0
  1116. package/src/features/replay/transport/BackendReplayTransport.ts +215 -0
  1117. package/src/features/replay/transport/CompositeReplayTransport.ts +82 -0
  1118. package/src/features/replay/transport/DesktopReplayTransport.ts +140 -0
  1119. package/src/features/replay/transport/ReplayTransport.ts +84 -0
  1120. package/src/features/session/BackendSessionAdapter.ts +1977 -0
  1121. package/src/features/session/DesktopSessionTransport.ts +130 -0
  1122. package/src/features/session/JourneyEventPipeline.ts +278 -0
  1123. package/src/features/session/ReplayCaptureManager.ts +1401 -0
  1124. package/src/features/session/ScrollTracker.ts +166 -0
  1125. package/src/features/session/SessionManager.ts +1687 -0
  1126. package/src/features/session/SyncDecisionEngine.ts +117 -0
  1127. package/src/features/session/frameLink.ts +41 -0
  1128. package/src/features/session/frameScrollState.ts +36 -0
  1129. package/src/features/session/index.ts +88 -0
  1130. package/src/features/session/nativeCapture.ts +43 -0
  1131. package/src/features/session/outboxFrameCapture.ts +79 -0
  1132. package/src/features/session/sessionTypes.ts +528 -0
  1133. package/src/features/session/viewportPlausibility.ts +134 -0
  1134. package/src/features/skan/skanBridge.ts +53 -0
  1135. package/src/features/skan/skanConversionManager.ts +173 -0
  1136. package/src/index.ts +206 -0
  1137. package/src/integrations/fetch/fetchInterceptor.ts +201 -0
  1138. package/src/integrations/navigation/screenTracker.ts +0 -0
  1139. package/src/integrations/network/bodyCapture.ts +328 -0
  1140. package/src/integrations/xhr/xhrInterceptor.ts +259 -0
  1141. package/src/metro/composeSourceMap.ts +104 -0
  1142. package/src/metro/featureModules.ts +143 -0
  1143. package/src/metro/index.ts +336 -0
  1144. package/src/metro/optionalDependencyStub.ts +13 -0
  1145. package/src/metro/optionalModules.ts +27 -0
  1146. package/src/metro/serializerCompose.ts +193 -0
  1147. package/src/pipeline/dispatcher/dispatcher.ts +22 -0
  1148. package/src/pipeline/envelope/envelope.ts +31 -0
  1149. package/src/pipeline/envelope/serializer.ts +14 -0
  1150. package/src/pipeline/processors/dedupe.ts +0 -0
  1151. package/src/pipeline/processors/enrich.ts +11 -0
  1152. package/src/pipeline/processors/sampling.ts +0 -0
  1153. package/src/pipeline/processors/sanitize.ts +23 -0
  1154. package/src/pipeline/queue/flushQueue.ts +144 -0
  1155. package/src/pipeline/queue/memoryQueue.ts +31 -0
  1156. package/src/pipeline/queue/persistentQueue.ts +385 -0
  1157. package/src/pipeline/queue/queuePolicy.ts +0 -0
  1158. package/src/pipeline/scheduler/flushScheduler.ts +47 -0
  1159. package/src/pipeline/scheduler/retryPolicy.ts +8 -0
  1160. package/src/public/ScaleBunErrorBoundary.tsx +91 -0
  1161. package/src/public/ScaleBunFacade.ts +2129 -0
  1162. package/src/public/ScaleBunProvider.tsx +91 -0
  1163. package/src/public/typedTracker.ts +123 -0
  1164. package/src/public/types.ts +362 -0
  1165. package/src/push/PushManager.ts +318 -0
  1166. package/src/push/adapters/ManualAdapter.ts +100 -0
  1167. package/src/push/adapters/NativeBridgeAdapter.ts +142 -0
  1168. package/src/push/adapters/NoopAdapter.ts +46 -0
  1169. package/src/push/adapters/RNFirebaseMessagingAdapter.ts +271 -0
  1170. package/src/push/adapters/loadMessaging.ts +24 -0
  1171. package/src/push/adapters/loadNotifee.ts +29 -0
  1172. package/src/push/adapters/selectAdapter.ts +176 -0
  1173. package/src/push/detect/capabilities.ts +124 -0
  1174. package/src/push/index.ts +22 -0
  1175. package/src/push/native/nativeNotifications.ts +207 -0
  1176. package/src/push/types.ts +159 -0
  1177. package/src/specs/NativeReplaySdk.ts +101 -0
  1178. package/src/specs/NativeScaleBunCrash.ts +64 -0
  1179. package/src/specs/NativeScaleBunInstallReferrer.ts +29 -0
  1180. package/src/specs/NativeScaleBunOta.ts +154 -0
  1181. package/src/specs/NativeScaleBunPerformance.ts +34 -0
  1182. package/src/specs/NativeScaleBunProfiler.ts +41 -0
  1183. package/src/specs/NativeScaleBunStorage.ts +59 -0
  1184. package/src/storage/StorageBackend.ts +272 -0
  1185. package/src/storage/db/sqlite.ts +0 -0
  1186. package/src/storage/files/fileStore.ts +0 -0
  1187. package/src/transport/auth/sdkSession.ts +27 -0
  1188. package/src/transport/backoff/exponentialJitter.ts +9 -0
  1189. package/src/transport/http/endpoints.ts +33 -0
  1190. package/src/transport/http/httpClient.ts +73 -0
  1191. package/src/transport/rateLimit/retryAfter.ts +0 -0
  1192. package/src/transport/signing/requestSigner.ts +0 -0
  1193. package/src/vendor/protocol/index.ts +391 -0
  1194. package/src/vendor/protocol/internal/validation.ts +497 -0
  1195. package/src/vendor/sdk-contracts/bridge.ts +162 -0
  1196. package/src/vendor/sdk-contracts/config.ts +192 -0
  1197. package/src/vendor/sdk-contracts/events.ts +50 -0
  1198. package/src/vendor/sdk-contracts/features.ts +42 -0
  1199. package/src/vendor/sdk-contracts/index.ts +98 -0
  1200. package/src/vendor/sdk-contracts/platform.ts +42 -0
  1201. package/src/vendor/sdk-contracts/session-boundary.ts +55 -0
  1202. package/src/vendor/sdk-contracts/session.ts +225 -0
  1203. package/src/vendor/vendor.manifest +10 -0
@@ -0,0 +1,3705 @@
1
+ /**
2
+ * ScaleBun SDK — Archetype-specific in-app renderers
3
+ *
4
+ * These renderers own mechanic content only. Presentation (modal, sheet, banner,
5
+ * fullscreen, anchored, or inline) stays in EngageInAppView so one mechanic can
6
+ * be mounted correctly in every container.
7
+ *
8
+ * Implemented archetypes (matching dashboard variant keys):
9
+ * spin_wheel → animated spinning prize wheel
10
+ * countdown_offer → live flip-style countdown clock
11
+ * scratch_card → tap-to-reveal scratch surface
12
+ * slot_reward → three-reel slot machine
13
+ * poll_card → single-choice option list
14
+ * nps_prompt → 0–10 NPS number row
15
+ * emoji_reaction_bar → emoji row picker
16
+ * rating_request → 1–5 star tap rating
17
+ * streak_reward → day-streak dot strip
18
+ * checklist_progress → tick-off checklist
19
+ * coupon_wallet → large coupon reveal chip
20
+ */
21
+
22
+ import React, { useEffect, useMemo, useRef, useState } from "react";
23
+ import {
24
+ AccessibilityInfo,
25
+ Animated,
26
+ Easing,
27
+ Image,
28
+ PanResponder,
29
+ Pressable,
30
+ ScrollView,
31
+ Text,
32
+ TextInput,
33
+ View,
34
+ type LayoutChangeEvent,
35
+ type GestureResponderEvent,
36
+ type NativeScrollEvent,
37
+ type NativeSyntheticEvent,
38
+ type TextStyle,
39
+ type ViewStyle,
40
+ } from "react-native";
41
+ import type { InAppBranchProps } from "./EngageInAppView";
42
+ import type { InAppGameOutcome } from "./engageTypes";
43
+ import {
44
+ addScratchedCell,
45
+ boundedInt,
46
+ clampPage,
47
+ gradeGameAnswer,
48
+ resolveSlotOutcome,
49
+ scoreGameAnswer,
50
+ scratchCellAt,
51
+ scratchProgress,
52
+ toggleGameSelection,
53
+ wheelTargetAngle,
54
+ } from "./engageGameLogic";
55
+ import { resolveInAppMediaHeight } from "./engageMediaSizing";
56
+ import { inAppOutcomeTerminalForCta } from "./engageOutcome";
57
+ import { requestNativeStoreReview } from "./engageStoreReviewBridge";
58
+
59
+ // ─── helpers ──────────────────────────────────────────────────────────────────
60
+
61
+ const ext = (props: InAppBranchProps): Record<string, unknown> =>
62
+ (props.spec.ext as Record<string, unknown>) ?? {};
63
+ const str = (v: unknown): string => (typeof v === "string" ? v : "");
64
+ const num = (v: unknown, fallback: number): number =>
65
+ typeof v === "number" ? v : fallback;
66
+ const arr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []);
67
+ const record = (v: unknown): Record<string, unknown> =>
68
+ v !== null && typeof v === "object" && !Array.isArray(v)
69
+ ? (v as Record<string, unknown>)
70
+ : {};
71
+ const labelOf = (v: unknown): string => {
72
+ const value = record(v);
73
+ return str(value.label ?? value.title ?? value.name ?? value.prompt ?? v);
74
+ };
75
+ const firstText = (
76
+ source: Record<string, unknown>,
77
+ keys: readonly string[],
78
+ ): string => {
79
+ for (const key of keys) {
80
+ const value = str(source[key]);
81
+ if (value) return value;
82
+ }
83
+ return "";
84
+ };
85
+ const variantOf = (props: InAppBranchProps): string =>
86
+ props.variantKey ||
87
+ str(
88
+ (props.spec as InAppBranchProps["spec"] & { variantKey?: unknown })
89
+ .variantKey,
90
+ );
91
+ const mediaUri = (value: unknown): string => {
92
+ const item = record(value);
93
+ return str(item.uri ?? item.url ?? item.imageUrl ?? item.image ?? value);
94
+ };
95
+
96
+ const gameUnavailableText = (error: unknown): string => {
97
+ if (error instanceof Error && error.message) return error.message;
98
+ return "Result unavailable. Please try again.";
99
+ };
100
+
101
+ const gameResultLabel = (outcome: InAppGameOutcome | null): string => {
102
+ if (!outcome) return "";
103
+ if (outcome.label) return outcome.label;
104
+ return typeof outcome.value === "string" || typeof outcome.value === "number"
105
+ ? String(outcome.value)
106
+ : "";
107
+ };
108
+
109
+ const presentTerminalOutcome = (
110
+ props: InAppBranchProps,
111
+ resultLabel?: string,
112
+ ): boolean => {
113
+ if (!props.onOutcome) return false;
114
+ props.onOutcome({
115
+ ...(resultLabel ? { resultLabel } : {}),
116
+ fallback: inAppOutcomeTerminalForCta(props.spec.cta),
117
+ });
118
+ return true;
119
+ };
120
+
121
+ async function requireGameOutcome(
122
+ props: InAppBranchProps,
123
+ attempt = 1,
124
+ input?: Record<string, unknown>,
125
+ ): Promise<InAppGameOutcome> {
126
+ if (!props.requestGameResult) throw new Error("Game result handler is not configured");
127
+ const result = await props.requestGameResult(attempt, input);
128
+ if (result.status === "unavailable") {
129
+ throw new Error(result.message || "Result unavailable. Please try again.");
130
+ }
131
+ return result.outcome;
132
+ }
133
+
134
+ function useReducedMotion(): boolean | null {
135
+ const [enabled, setEnabled] = useState<boolean | null>(null);
136
+
137
+ useEffect(() => {
138
+ let mounted = true;
139
+ AccessibilityInfo.isReduceMotionEnabled().then((value) => {
140
+ if (mounted) setEnabled(value);
141
+ });
142
+ const subscription = AccessibilityInfo.addEventListener(
143
+ "reduceMotionChanged",
144
+ setEnabled,
145
+ );
146
+ return () => {
147
+ mounted = false;
148
+ subscription.remove();
149
+ };
150
+ }, []);
151
+
152
+ return enabled;
153
+ }
154
+
155
+ function Title({
156
+ text,
157
+ theme,
158
+ }: {
159
+ text?: string;
160
+ theme: InAppBranchProps["theme"];
161
+ }) {
162
+ if (!text) return null;
163
+ return <Text style={[sh.title, { color: theme.titleColor }]}>{text}</Text>;
164
+ }
165
+ function Body({
166
+ text,
167
+ theme,
168
+ }: {
169
+ text?: string;
170
+ theme: InAppBranchProps["theme"];
171
+ }) {
172
+ if (!text) return null;
173
+ return <Text style={[sh.body, { color: theme.bodyColor }]}>{text}</Text>;
174
+ }
175
+ function PrimaryBtn({
176
+ label,
177
+ accent,
178
+ onPress,
179
+ disabled = false,
180
+ }: {
181
+ label: string;
182
+ accent: string;
183
+ onPress: () => void;
184
+ disabled?: boolean;
185
+ }) {
186
+ return (
187
+ <Pressable
188
+ style={[
189
+ sh.primaryBtn,
190
+ { backgroundColor: accent, opacity: disabled ? 0.65 : 1 },
191
+ ]}
192
+ onPress={onPress}
193
+ disabled={disabled}
194
+ accessibilityRole="button"
195
+ accessibilityLabel={label}
196
+ accessibilityState={{ disabled }}
197
+ >
198
+ <Text style={sh.primaryText}>{label}</Text>
199
+ </Pressable>
200
+ );
201
+ }
202
+ function SecondaryBtn({
203
+ label,
204
+ onPress,
205
+ color,
206
+ }: {
207
+ label: string;
208
+ onPress: () => void;
209
+ color: string;
210
+ }) {
211
+ return (
212
+ <Pressable
213
+ style={sh.secondaryBtn}
214
+ onPress={onPress}
215
+ accessibilityRole="button"
216
+ accessibilityLabel={label}
217
+ >
218
+ <Text style={[sh.secondaryText, { color }]}>{label}</Text>
219
+ </Pressable>
220
+ );
221
+ }
222
+ function CtaArea({
223
+ beforePrimary,
224
+ onPrimary,
225
+ ...props
226
+ }: InAppBranchProps & { beforePrimary?: () => void; onPrimary?: () => void }) {
227
+ const { spec, theme, onCta, onDismiss } = props;
228
+ if (!spec.cta && !spec.secondaryCta) return null;
229
+ return (
230
+ <View style={sh.ctaRow}>
231
+ {spec.secondaryCta ? (
232
+ <SecondaryBtn
233
+ label={spec.secondaryCta.label}
234
+ onPress={onDismiss}
235
+ color={theme.secondaryColor}
236
+ />
237
+ ) : null}
238
+ {spec.cta ? (
239
+ <PrimaryBtn
240
+ label={spec.cta.label}
241
+ accent={theme.accent}
242
+ onPress={() => {
243
+ beforePrimary?.();
244
+ if (onPrimary) onPrimary();
245
+ else if (presentTerminalOutcome(props)) return;
246
+ else if (spec.cta!.action === "dismiss") onDismiss();
247
+ else onCta(spec.cta!);
248
+ }}
249
+ />
250
+ ) : null}
251
+ </View>
252
+ );
253
+ }
254
+
255
+ // ─── 1. Spin Wheel ────────────────────────────────────────────────────────────
256
+
257
+ const WHEEL_COLORS = [
258
+ "#6C5CE7",
259
+ "#8B7CF6",
260
+ "#F5A524",
261
+ "#E85D75",
262
+ "#4834D4",
263
+ "#00A896",
264
+ ];
265
+ const WHEEL_MAX_SIZE = 264;
266
+ const MAX_WHEEL_SEGMENTS = 8;
267
+
268
+ export function resolveWheelDiameter(availableWidth: number): number {
269
+ return Number.isFinite(availableWidth) && availableWidth > 0
270
+ ? Math.min(WHEEL_MAX_SIZE, Math.floor(availableWidth))
271
+ : 0;
272
+ }
273
+
274
+ export type WheelSegment = {
275
+ id: string;
276
+ label: string;
277
+ fullLabel: string;
278
+ color: string;
279
+ sourceIndex: number;
280
+ };
281
+ type WheelOutcome = { index: number; label: string; resolved: boolean };
282
+
283
+ interface WheelOutcomeIdentity {
284
+ explicitIndex: number;
285
+ resultId: string;
286
+ resultLabel: string;
287
+ }
288
+
289
+ function wheelOutcomeIdentity(source: Record<string, unknown>): WheelOutcomeIdentity {
290
+ const resultValue = source.resultState ?? source.rewardResult ?? source.result ?? source.reward;
291
+ const result = record(resultValue);
292
+ return {
293
+ explicitIndex: num(
294
+ source.winningIndex ?? source.resultIndex ?? source.selectedIndex ?? result.index,
295
+ -1,
296
+ ),
297
+ resultId: str(
298
+ source.winningId ?? source.segmentId ?? result.segmentId ?? result.id ?? result.value,
299
+ ),
300
+ resultLabel: labelOf(resultValue),
301
+ };
302
+ }
303
+
304
+ function findWheelOutcomeIndex(
305
+ segments: readonly WheelSegment[],
306
+ identity: WheelOutcomeIdentity,
307
+ ): number {
308
+ if (identity.explicitIndex >= 0) {
309
+ const explicit = segments.findIndex(
310
+ ({ sourceIndex }) => sourceIndex === Math.floor(identity.explicitIndex),
311
+ );
312
+ if (explicit >= 0) return explicit;
313
+ }
314
+ if (identity.resultId) {
315
+ const byId = segments.findIndex(({ id }) => id === identity.resultId);
316
+ if (byId >= 0) return byId;
317
+ }
318
+ if (!identity.resultLabel) return -1;
319
+ const normalizedResult = normalizedWheelValue(identity.resultLabel);
320
+ const normalizedLabels = segments.map((segment) =>
321
+ [segment.fullLabel, segment.label].map(normalizedWheelValue),
322
+ );
323
+ const exact = normalizedLabels.findIndex((labels) =>
324
+ labels.includes(normalizedResult),
325
+ );
326
+ if (exact >= 0) return exact;
327
+ let bestIndex = -1;
328
+ let bestLength = -1;
329
+ normalizedLabels.forEach((labels, index) => {
330
+ const length = Math.max(
331
+ ...labels.map((label) => (normalizedResult.includes(label) ? label.length : -1)),
332
+ );
333
+ if (length > bestLength) {
334
+ bestIndex = index;
335
+ bestLength = length;
336
+ }
337
+ });
338
+ return bestIndex;
339
+ }
340
+
341
+ export function prepareWheelSegments(
342
+ value: unknown,
343
+ source: Record<string, unknown> = {},
344
+ ): WheelSegment[] {
345
+ const segments = arr(value)
346
+ .map((entry, index) => {
347
+ const item = record(entry);
348
+ const fullLabel = labelOf(entry);
349
+ return {
350
+ id: str(item.id ?? item.value ?? item.code) || String(index),
351
+ label: str(item.shortLabel) || fullLabel,
352
+ fullLabel,
353
+ color: str(item.color ?? item.backgroundColor),
354
+ sourceIndex: index,
355
+ };
356
+ })
357
+ .filter(({ label }) => Boolean(label));
358
+ if (segments.length <= MAX_WHEEL_SEGMENTS) return segments;
359
+
360
+ const winnerIndex = findWheelOutcomeIndex(segments, wheelOutcomeIdentity(source));
361
+ const winner = winnerIndex >= 0 ? segments[winnerIndex] : undefined;
362
+ const visible = segments.slice(0, MAX_WHEEL_SEGMENTS);
363
+ if (winner && !visible.includes(winner)) visible[MAX_WHEEL_SEGMENTS - 1] = winner;
364
+ return visible;
365
+ }
366
+
367
+ const normalizedWheelValue = (value: string): string =>
368
+ value.toLowerCase().replace(/\s+/g, " ").trim();
369
+
370
+ export function resolveWheelOutcome(
371
+ segments: readonly WheelSegment[],
372
+ source: Record<string, unknown>,
373
+ ): WheelOutcome {
374
+ const identity = wheelOutcomeIdentity(source);
375
+ const index = findWheelOutcomeIndex(segments, identity);
376
+
377
+ const resolved = index >= 0;
378
+ return {
379
+ index: resolved ? index : 0,
380
+ label: identity.resultLabel || (resolved ? segments[index]?.fullLabel ?? "" : ""),
381
+ resolved,
382
+ };
383
+ }
384
+
385
+ export function SpinWheelInApp(props: InAppBranchProps): React.ReactElement {
386
+ const { spec, theme, onDismiss, onCta } = props;
387
+ const e = ext(props);
388
+ const [gameOutcome, setGameOutcome] = useState<InAppGameOutcome | null>(null);
389
+ const [gameError, setGameError] = useState("");
390
+ const outcomeSource: Record<string, unknown> = gameOutcome
391
+ ? {
392
+ winningId: gameOutcome.segmentId || gameOutcome.id,
393
+ winningIndex: gameOutcome.segmentIndex,
394
+ resultState: { id: gameOutcome.id, label: gameResultLabel(gameOutcome) },
395
+ }
396
+ : {};
397
+ const configuredSegments = prepareWheelSegments(e.segments, outcomeSource);
398
+ const segments =
399
+ configuredSegments.length > 0
400
+ ? configuredSegments
401
+ : prepareWheelSegments([
402
+ "10% OFF",
403
+ "FREE GIFT",
404
+ "20% OFF",
405
+ "TRY AGAIN",
406
+ "5% OFF",
407
+ "JACKPOT!",
408
+ ]);
409
+ const n = segments.length;
410
+ const sliceDeg = 360 / n;
411
+ const [wheelSize, setWheelSize] = useState(WHEEL_MAX_SIZE);
412
+ const wheelRadius = wheelSize / 2;
413
+ const wheelScale = wheelSize / WHEEL_MAX_SIZE;
414
+ const labelRadius = wheelRadius * 0.65;
415
+ const hubSize = Math.max(30, 40 * wheelScale);
416
+ const wedgeHalfWidth = Math.ceil(
417
+ wheelRadius * Math.tan(Math.PI / Math.max(n, 3)),
418
+ );
419
+ const outcome = resolveWheelOutcome(segments, outcomeSource);
420
+
421
+ const spin = useRef(new Animated.Value(0)).current;
422
+ const reducedMotion = useReducedMotion();
423
+ const angleRef = useRef(0);
424
+ const [phase, setPhase] = useState<
425
+ "idle" | "resolving" | "spinning" | "result" | "claimed"
426
+ >("idle");
427
+
428
+ useEffect(() => () => spin.stopAnimation(), [spin]);
429
+
430
+ const doSpin = async () => {
431
+ if (phase !== "idle") return;
432
+ setGameError("");
433
+ setPhase("resolving");
434
+ props.onInteraction?.({ mechanic: "spin_wheel", phase: "started", attempt: 1 });
435
+ let remoteOutcome: InAppGameOutcome;
436
+ try {
437
+ remoteOutcome = await requireGameOutcome(props);
438
+ } catch (error) {
439
+ setGameError(gameUnavailableText(error));
440
+ setPhase("idle");
441
+ return;
442
+ }
443
+ const remoteSource: Record<string, unknown> = {
444
+ winningId: remoteOutcome.segmentId || remoteOutcome.id,
445
+ winningIndex: remoteOutcome.segmentIndex,
446
+ resultState: { id: remoteOutcome.id, label: gameResultLabel(remoteOutcome) },
447
+ };
448
+ const remoteSegments = prepareWheelSegments(e.segments, remoteSource);
449
+ const displayedSegments = remoteSegments.length > 0 ? remoteSegments : segments;
450
+ const remoteResult = resolveWheelOutcome(displayedSegments, remoteSource);
451
+ if (!remoteResult.resolved) {
452
+ setGameError("The backend result does not match a wheel segment.");
453
+ setPhase("idle");
454
+ return;
455
+ }
456
+ setGameOutcome(remoteOutcome);
457
+ setPhase("spinning");
458
+ const from = angleRef.current;
459
+ // Animation is visual only; the customer backend's result selects the segment.
460
+ const to = wheelTargetAngle(displayedSegments.length, remoteResult.index, from);
461
+ angleRef.current = to;
462
+ spin.setValue(from);
463
+ Animated.timing(spin, {
464
+ toValue: to,
465
+ duration: reducedMotion === false ? 3600 : 0,
466
+ easing: Easing.out(Easing.cubic),
467
+ useNativeDriver: true,
468
+ }).start(({ finished }) => {
469
+ if (!finished) return;
470
+ setPhase("result");
471
+ props.onInteraction?.({
472
+ mechanic: "spin_wheel",
473
+ phase: "resolved",
474
+ attempt: 1,
475
+ result: remoteResult.label || undefined,
476
+ meta: { backendResolved: true, segmentIndex: remoteResult.index },
477
+ });
478
+ AccessibilityInfo.announceForAccessibility(
479
+ remoteResult.label || "Spin complete",
480
+ );
481
+ presentTerminalOutcome(props, remoteResult.label);
482
+ });
483
+ };
484
+
485
+ const rotate = spin.interpolate({
486
+ inputRange: [0, 360],
487
+ outputRange: ["0deg", "360deg"],
488
+ extrapolate: "extend",
489
+ });
490
+ const counterRotate = spin.interpolate({
491
+ inputRange: [0, 360],
492
+ outputRange: ["0deg", "-360deg"],
493
+ extrapolate: "extend",
494
+ });
495
+ const onWheelLayout = ({ nativeEvent }: LayoutChangeEvent): void => {
496
+ const measured = resolveWheelDiameter(nativeEvent.layout.width);
497
+ if (measured > 0) {
498
+ setWheelSize((current) => (current === measured ? current : measured));
499
+ }
500
+ };
501
+ const spinLabel =
502
+ str(e.spinLabel) ||
503
+ (/spin/i.test(spec.cta?.label ?? "") ? spec.cta?.label : "") ||
504
+ "SPIN";
505
+ const claimLabel =
506
+ str(e.claimLabel ?? e.ctaLabel) ||
507
+ (/spin/i.test(spec.cta?.label ?? "")
508
+ ? gameOutcome
509
+ ? "CLAIM REWARD"
510
+ : "CONTINUE"
511
+ : spec.cta?.label) ||
512
+ "CONTINUE";
513
+ const handleClaim = () => {
514
+ props.onInteraction?.({
515
+ mechanic: "spin_wheel",
516
+ phase: "claimed",
517
+ attempt: 1,
518
+ result: outcome.label || undefined,
519
+ });
520
+ if (!spec.cta) {
521
+ onDismiss();
522
+ return;
523
+ }
524
+ if (spec.cta.action === "dismiss") {
525
+ onDismiss();
526
+ return;
527
+ }
528
+ onCta(spec.cta);
529
+ setPhase("claimed");
530
+ };
531
+
532
+ return (
533
+ <View style={sh.content}>
534
+ <Title text={spec.title} theme={theme} />
535
+ <Body text={spec.body} theme={theme} />
536
+ <View style={wh.stage} onLayout={onWheelLayout}>
537
+ {/* Pointer */}
538
+ <View style={[wh.pointer, { borderTopColor: theme.accent }]} />
539
+ <Animated.View
540
+ accessibilityElementsHidden
541
+ importantForAccessibility="no-hide-descendants"
542
+ style={[
543
+ wh.wheel,
544
+ {
545
+ backgroundColor: theme.couponBg,
546
+ borderColor: theme.accent,
547
+ borderRadius: wheelRadius,
548
+ height: wheelSize,
549
+ transform: [{ rotate }],
550
+ width: wheelSize,
551
+ },
552
+ ]}
553
+ >
554
+ {segments.map((segment, i) => {
555
+ const angle = sliceDeg * i;
556
+ const color = segment.color || WHEEL_COLORS[i % WHEEL_COLORS.length] || theme.accent;
557
+ const labelWidth = Math.max(
558
+ 28,
559
+ Math.min(
560
+ 84 * wheelScale,
561
+ 2 * labelRadius * Math.sin(Math.PI / Math.max(n, 2)) -
562
+ 8 * wheelScale,
563
+ ),
564
+ );
565
+ return (
566
+ <React.Fragment key={`${segment.id}-${i}`}>
567
+ {n === 1 ? (
568
+ <View style={[wh.singleWedge, { backgroundColor: color }]} />
569
+ ) : n === 2 ? (
570
+ <View
571
+ style={[
572
+ wh.halfWedge,
573
+ {
574
+ backgroundColor: color,
575
+ height: wheelRadius,
576
+ top: i * wheelRadius,
577
+ width: wheelSize,
578
+ },
579
+ ]}
580
+ />
581
+ ) : (
582
+ <View
583
+ style={[
584
+ wh.wedgeOrbit,
585
+ {
586
+ height: wheelSize,
587
+ transform: [{ rotate: `${angle + 180}deg` }],
588
+ width: wheelSize,
589
+ },
590
+ ]}
591
+ >
592
+ <View
593
+ style={[
594
+ wh.wedge,
595
+ {
596
+ borderBottomWidth: wheelRadius,
597
+ borderLeftWidth: wedgeHalfWidth,
598
+ borderRightWidth: wedgeHalfWidth,
599
+ borderBottomColor: color,
600
+ left: wheelRadius - wedgeHalfWidth,
601
+ top: wheelRadius,
602
+ },
603
+ ]}
604
+ />
605
+ </View>
606
+ )}
607
+ <View
608
+ style={[
609
+ wh.labelOrbit,
610
+ {
611
+ height: wheelSize,
612
+ transform: [{ rotate: `${angle}deg` }],
613
+ width: wheelSize,
614
+ },
615
+ ]}
616
+ >
617
+ <Animated.View
618
+ style={[
619
+ wh.segment,
620
+ {
621
+ left: wheelRadius - labelWidth / 2,
622
+ minHeight: 36 * wheelScale,
623
+ top: 28 * wheelScale,
624
+ width: labelWidth,
625
+ transform: [
626
+ { rotate: `${-angle}deg` },
627
+ { rotate: counterRotate },
628
+ ],
629
+ },
630
+ ]}
631
+ >
632
+ <Text numberOfLines={2} adjustsFontSizeToFit minimumFontScale={0.7} style={wh.segmentText}>
633
+ {segment.label}
634
+ </Text>
635
+ </Animated.View>
636
+ </View>
637
+ </React.Fragment>
638
+ );
639
+ })}
640
+ <View
641
+ style={[
642
+ wh.hub,
643
+ {
644
+ borderColor: theme.accent,
645
+ borderRadius: hubSize / 2,
646
+ height: hubSize,
647
+ left: wheelRadius - hubSize / 2,
648
+ top: wheelRadius - hubSize / 2,
649
+ width: hubSize,
650
+ },
651
+ ]}
652
+ >
653
+ <Text style={[wh.hubText, { color: theme.accent }]}>★</Text>
654
+ </View>
655
+ </Animated.View>
656
+ </View>
657
+ {phase === "result" || phase === "claimed" ? (
658
+ <View
659
+ style={[wh.result, { backgroundColor: theme.accent }]}
660
+ accessibilityRole="summary"
661
+ accessibilityLiveRegion="polite"
662
+ >
663
+ <Text style={wh.resultEyebrow}>
664
+ {phase === "claimed"
665
+ ? str(e.claimedLabel) || (outcome.resolved ? "CLAIMED" : "DONE")
666
+ : outcome.resolved
667
+ ? str(e.resultLabel) || "YOU LANDED ON"
668
+ : str(e.resultLabel) || "SPIN COMPLETE"}
669
+ </Text>
670
+ <Text style={wh.resultText}>
671
+ {phase === "claimed"
672
+ ? str(e.claimedState) ||
673
+ outcome.label ||
674
+ (outcome.resolved ? "Reward claimed" : "Action complete")
675
+ : outcome.label || "Your result will be confirmed next"}
676
+ </Text>
677
+ </View>
678
+ ) : null}
679
+ {phase === "idle" && gameError ? (
680
+ <Text style={[wh.unavailable, { color: theme.secondaryColor }]}>
681
+ {gameError}
682
+ </Text>
683
+ ) : null}
684
+ <View style={sh.ctaRow}>
685
+ {phase === "idle" || phase === "resolving" || phase === "spinning" ? (
686
+ <PrimaryBtn
687
+ label={phase === "resolving" ? "CHECKING…" : phase === "spinning" ? "SPINNING…" : gameError ? "RETRY" : spinLabel}
688
+ accent={theme.accent}
689
+ onPress={() => { void doSpin(); }}
690
+ disabled={phase === "resolving" || phase === "spinning"}
691
+ />
692
+ ) : phase === "result" ? (
693
+ <>
694
+ {spec.secondaryCta ? (
695
+ <SecondaryBtn
696
+ label={spec.secondaryCta.label}
697
+ onPress={onDismiss}
698
+ color={theme.secondaryColor}
699
+ />
700
+ ) : null}
701
+ <PrimaryBtn
702
+ label={claimLabel}
703
+ accent={theme.accent}
704
+ onPress={handleClaim}
705
+ />
706
+ </>
707
+ ) : (
708
+ <PrimaryBtn label="DONE" accent={theme.accent} onPress={onDismiss} />
709
+ )}
710
+ </View>
711
+ </View>
712
+ );
713
+ }
714
+
715
+ const wh: Record<string, ViewStyle & TextStyle> = {
716
+ stage: { alignItems: "center", marginVertical: 12 } as any,
717
+ pointer: {
718
+ width: 0,
719
+ height: 0,
720
+ borderLeftWidth: 12,
721
+ borderRightWidth: 12,
722
+ borderTopWidth: 24,
723
+ borderLeftColor: "transparent",
724
+ borderRightColor: "transparent",
725
+ zIndex: 10,
726
+ marginBottom: -4,
727
+ } as any,
728
+ wheel: {
729
+ borderWidth: 5,
730
+ overflow: "hidden",
731
+ position: "relative",
732
+ shadowColor: "#000",
733
+ shadowOpacity: 0.2,
734
+ shadowRadius: 10,
735
+ shadowOffset: { width: 0, height: 4 },
736
+ elevation: 6,
737
+ } as any,
738
+ wedgeOrbit: {
739
+ position: "absolute",
740
+ left: 0,
741
+ top: 0,
742
+ } as any,
743
+ wedge: {
744
+ position: "absolute",
745
+ width: 0,
746
+ height: 0,
747
+ borderLeftColor: "transparent",
748
+ borderRightColor: "transparent",
749
+ } as any,
750
+ singleWedge: { position: "absolute", inset: 0 } as any,
751
+ halfWedge: {
752
+ position: "absolute",
753
+ left: 0,
754
+ } as any,
755
+ labelOrbit: {
756
+ position: "absolute",
757
+ left: 0,
758
+ top: 0,
759
+ } as any,
760
+ segment: {
761
+ position: "absolute",
762
+ borderRadius: 10,
763
+ paddingHorizontal: 5,
764
+ justifyContent: "center",
765
+ alignItems: "center",
766
+ backgroundColor: "rgba(0,0,0,0.32)",
767
+ } as any,
768
+ segmentText: {
769
+ color: "#FFFFFF",
770
+ fontSize: 10,
771
+ fontWeight: "800",
772
+ textAlign: "center",
773
+ lineHeight: 12,
774
+ textShadowColor: "rgba(0,0,0,0.25)",
775
+ textShadowOffset: { width: 0, height: 1 },
776
+ textShadowRadius: 2,
777
+ } as any,
778
+ hub: {
779
+ position: "absolute",
780
+ backgroundColor: "#FFFFFF",
781
+ borderWidth: 3,
782
+ alignItems: "center",
783
+ justifyContent: "center",
784
+ } as any,
785
+ hubText: { fontSize: 16, fontWeight: "900" } as any,
786
+ result: {
787
+ borderRadius: 12,
788
+ paddingVertical: 10,
789
+ paddingHorizontal: 18,
790
+ alignSelf: "center",
791
+ marginBottom: 10,
792
+ } as any,
793
+ resultText: {
794
+ color: "#fff",
795
+ fontWeight: "800",
796
+ fontSize: 16,
797
+ textAlign: "center",
798
+ } as any,
799
+ resultEyebrow: {
800
+ color: "rgba(255,255,255,0.78)",
801
+ fontWeight: "700",
802
+ fontSize: 10,
803
+ letterSpacing: 1,
804
+ textAlign: "center",
805
+ marginBottom: 2,
806
+ } as any,
807
+ unavailable: { fontSize: 11, lineHeight: 15, textAlign: "center", marginBottom: 4 } as any,
808
+ };
809
+
810
+ // ─── 2. Countdown Timer ───────────────────────────────────────────────────────
811
+
812
+ export function resolveCountdownDeadline(
813
+ expiryValue: string,
814
+ now = Date.now(),
815
+ ): number {
816
+ const duration = expiryValue.match(/^(\d{1,2}):(\d{2}):(\d{2})$/);
817
+ if (duration) {
818
+ const [, hours = "0", minutes = "0", seconds = "0"] = duration;
819
+ return (
820
+ now +
821
+ (Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds)) * 1000
822
+ );
823
+ }
824
+ const timestamp = Date.parse(expiryValue);
825
+ return Number.isFinite(timestamp) ? timestamp : now + 30 * 60 * 1000;
826
+ }
827
+
828
+ function useCountdown(expiryValue: string) {
829
+ const deadline = useMemo(
830
+ () => resolveCountdownDeadline(expiryValue),
831
+ [expiryValue],
832
+ );
833
+ const getMs = () => Math.max(0, deadline - Date.now());
834
+ const [ms, setMs] = useState(getMs);
835
+ useEffect(() => {
836
+ const id = setInterval(() => setMs(getMs()), 1000);
837
+ return () => clearInterval(id);
838
+ }, [deadline]);
839
+ const totalSec = Math.floor(ms / 1000);
840
+ const h = Math.floor(totalSec / 3600);
841
+ const m = Math.floor((totalSec % 3600) / 60);
842
+ const s = totalSec % 60;
843
+ return { h, m, s, done: ms === 0 };
844
+ }
845
+
846
+ function FlipUnit({ value, label }: { value: number; label: string }) {
847
+ const v = String(value).padStart(2, "0");
848
+ return (
849
+ <View style={cd.unit}>
850
+ <View style={cd.box}>
851
+ <Text
852
+ adjustsFontSizeToFit
853
+ minimumFontScale={0.65}
854
+ numberOfLines={1}
855
+ style={cd.num}
856
+ >
857
+ {v}
858
+ </Text>
859
+ </View>
860
+ <Text style={cd.lbl}>{label}</Text>
861
+ </View>
862
+ );
863
+ }
864
+
865
+ export function CountdownInApp(props: InAppBranchProps): React.ReactElement {
866
+ const { spec, theme } = props;
867
+ const e = ext(props);
868
+ const expiry =
869
+ str(e.expiryTime) ||
870
+ str(e.expiry);
871
+ const units = num(e.countdownUnits, 3);
872
+ const { h, m, s, done } = useCountdown(expiry);
873
+ return (
874
+ <View style={sh.content}>
875
+ <Title text={spec.title} theme={theme} />
876
+ <Body text={done ? "This offer has expired." : spec.body} theme={theme} />
877
+ {!done ? (
878
+ <View style={cd.row}>
879
+ {units >= 3 ? <FlipUnit value={h} label="HRS" /> : null}
880
+ {units >= 2 ? (
881
+ <>
882
+ <Text allowFontScaling={false} style={[cd.sep, { color: theme.accent }]}>:</Text>
883
+ <FlipUnit value={m} label="MIN" />
884
+ </>
885
+ ) : null}
886
+ <Text allowFontScaling={false} style={[cd.sep, { color: theme.accent }]}>:</Text>
887
+ <FlipUnit value={s} label="SEC" />
888
+ </View>
889
+ ) : null}
890
+ <CtaArea {...props} />
891
+ </View>
892
+ );
893
+ }
894
+
895
+ const cd: Record<string, ViewStyle & TextStyle> = {
896
+ row: {
897
+ flexDirection: "row",
898
+ alignItems: "flex-start",
899
+ justifyContent: "center",
900
+ marginVertical: 14,
901
+ width: "100%",
902
+ } as any,
903
+ unit: { alignItems: "center", flex: 1, marginHorizontal: 2, minWidth: 0 } as any,
904
+ box: {
905
+ backgroundColor: "#111827",
906
+ borderRadius: 10,
907
+ paddingVertical: 8,
908
+ paddingHorizontal: 4,
909
+ width: "100%",
910
+ } as any,
911
+ num: {
912
+ fontSize: 24,
913
+ fontWeight: "800",
914
+ color: "#fff",
915
+ textAlign: "center",
916
+ fontVariant: ["tabular-nums"],
917
+ } as any,
918
+ lbl: {
919
+ fontSize: 9,
920
+ color: "#9CA3AF",
921
+ marginTop: 4,
922
+ textTransform: "uppercase",
923
+ letterSpacing: 0.5,
924
+ } as any,
925
+ sep: {
926
+ fontSize: 28,
927
+ fontWeight: "800",
928
+ paddingHorizontal: 2,
929
+ paddingTop: 4,
930
+ } as any,
931
+ };
932
+
933
+ // ─── Carousel / Swipe Deck ───────────────────────────────────────────────────
934
+
935
+ interface GameCard {
936
+ title: string;
937
+ body: string;
938
+ image: string;
939
+ id: string;
940
+ }
941
+
942
+ function gameCards(value: unknown): GameCard[] {
943
+ return arr(value).slice(0, 20).map((item, index) => {
944
+ const card = record(item);
945
+ return {
946
+ id: str(card.id ?? card.value) || String(index),
947
+ title: labelOf(item),
948
+ body: firstText(card, ["body", "description", "message", "price"]),
949
+ image: mediaUri(card.image ?? card.imageUrl ?? card.cardImage ?? card.productImage),
950
+ };
951
+ });
952
+ }
953
+
954
+ export function CarouselDeckInApp(props: InAppBranchProps): React.ReactElement {
955
+ if (variantOf(props) === "swipe_deck") return <SwipeDeckInApp {...props} />;
956
+ const { spec, theme } = props;
957
+ const e = ext(props);
958
+ const slides = gameCards(e.slides ?? e.cards ?? e.products ?? props.spec.assets?.products);
959
+ const mediaHeight = resolveInAppMediaHeight(spec, 104, 240);
960
+ const [page, setPage] = useState(0);
961
+ const [pageWidth, setPageWidth] = useState(280);
962
+
963
+ const onLayout = (event: LayoutChangeEvent) => {
964
+ const width = Math.round(event.nativeEvent.layout.width);
965
+ if (width > 0) setPageWidth(width);
966
+ };
967
+ const onPage = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
968
+ const width = event.nativeEvent.layoutMeasurement.width || pageWidth;
969
+ setPage(clampPage(event.nativeEvent.contentOffset.x, width, slides.length));
970
+ };
971
+
972
+ return (
973
+ <View style={sh.content}>
974
+ <Title text={spec.title} theme={theme} />
975
+ <Body text={spec.body} theme={theme} />
976
+ {slides.length > 0 ? (
977
+ <>
978
+ <ScrollView
979
+ horizontal
980
+ pagingEnabled
981
+ showsHorizontalScrollIndicator={false}
982
+ onLayout={onLayout}
983
+ onMomentumScrollEnd={onPage}
984
+ style={car.viewport}
985
+ >
986
+ {slides.map((slide, index) => {
987
+ const image =
988
+ slide.image ||
989
+ (index === 0
990
+ ? str(spec.assets?.image) || str(spec.imageUrl)
991
+ : "");
992
+ return (
993
+ <View
994
+ key={`${slide.title}-${index}`}
995
+ style={{ width: pageWidth }}
996
+ >
997
+ <View
998
+ style={[
999
+ car.card,
1000
+ {
1001
+ backgroundColor: theme.couponBg,
1002
+ borderColor: theme.couponBorder,
1003
+ },
1004
+ ]}
1005
+ >
1006
+ {image ? (
1007
+ <Image
1008
+ source={{ uri: image }}
1009
+ resizeMode="cover"
1010
+ style={[
1011
+ car.image,
1012
+ mediaHeight === undefined ? null : { height: mediaHeight },
1013
+ ] as any}
1014
+ accessibilityLabel={slide.title || `Slide ${index + 1}`}
1015
+ />
1016
+ ) : null}
1017
+ {slide.title ? (
1018
+ <Text style={[car.title, { color: theme.titleColor }]}>
1019
+ {slide.title}
1020
+ </Text>
1021
+ ) : null}
1022
+ {slide.body ? (
1023
+ <Text style={[car.body, { color: theme.bodyColor }]}>
1024
+ {slide.body}
1025
+ </Text>
1026
+ ) : null}
1027
+ </View>
1028
+ </View>
1029
+ );
1030
+ })}
1031
+ </ScrollView>
1032
+ {slides.length > 1 ? (
1033
+ <View
1034
+ style={car.dots}
1035
+ accessibilityLabel={`Slide ${page + 1} of ${slides.length}`}
1036
+ >
1037
+ {slides.map((_, index) => (
1038
+ <View
1039
+ key={index}
1040
+ style={[
1041
+ car.dot,
1042
+ {
1043
+ backgroundColor:
1044
+ index === page ? theme.accent : theme.couponBorder,
1045
+ },
1046
+ ]}
1047
+ />
1048
+ ))}
1049
+ </View>
1050
+ ) : null}
1051
+ </>
1052
+ ) : null}
1053
+ <CtaArea {...props} />
1054
+ </View>
1055
+ );
1056
+ }
1057
+
1058
+ const car: Record<string, ViewStyle & TextStyle> = {
1059
+ viewport: { width: "100%", marginTop: 10 } as any,
1060
+ card: {
1061
+ minHeight: 160,
1062
+ padding: 12,
1063
+ borderRadius: 14,
1064
+ borderWidth: 1,
1065
+ overflow: "hidden",
1066
+ } as any,
1067
+ image: {
1068
+ width: "100%",
1069
+ height: 104,
1070
+ borderRadius: 10,
1071
+ marginBottom: 10,
1072
+ } as any,
1073
+ title: { fontSize: 15, lineHeight: 20, fontWeight: "700" } as any,
1074
+ body: { fontSize: 12, lineHeight: 18, marginTop: 3 } as any,
1075
+ dots: {
1076
+ flexDirection: "row",
1077
+ flexWrap: "wrap",
1078
+ justifyContent: "center",
1079
+ gap: 6,
1080
+ marginTop: 10,
1081
+ } as any,
1082
+ dot: { width: 7, height: 7, borderRadius: 4 } as any,
1083
+ };
1084
+
1085
+ function SwipeDeckInApp(props: InAppBranchProps): React.ReactElement {
1086
+ const { spec, theme } = props;
1087
+ const e = ext(props);
1088
+ const cards = gameCards(e.cards ?? e.products ?? props.spec.assets?.products);
1089
+ const mediaHeight = resolveInAppMediaHeight(spec, 132, 300);
1090
+ const [index, setIndex] = useState(0);
1091
+ const [decisions, setDecisions] = useState<Array<{ id: string; decision: "keep" | "skip" }>>([]);
1092
+ const dragX = useRef(new Animated.Value(0)).current;
1093
+ const reducedMotion = useReducedMotion();
1094
+ const decideRef = useRef<(decision: "keep" | "skip") => void>(() => {});
1095
+ const decidingRef = useRef(false);
1096
+ const current = cards[index];
1097
+ const swipeActions = record(e.swipeActions);
1098
+ const skipLabel = str(swipeActions.skip ?? swipeActions.reject) || "SKIP";
1099
+ const keepLabel = str(swipeActions.keep ?? swipeActions.accept) || "KEEP";
1100
+
1101
+ const decide = (decision: "keep" | "skip") => {
1102
+ if (!current || decidingRef.current) return;
1103
+ decidingRef.current = true;
1104
+ const card = current;
1105
+ Animated.timing(dragX, {
1106
+ toValue: decision === "keep" ? 420 : -420,
1107
+ duration: reducedMotion === false ? 180 : 0,
1108
+ useNativeDriver: false,
1109
+ }).start(({ finished }) => {
1110
+ decidingRef.current = false;
1111
+ if (!finished) {
1112
+ dragX.setValue(0);
1113
+ return;
1114
+ }
1115
+ props.onInteraction?.({
1116
+ mechanic: "swipe_deck",
1117
+ phase: "decision",
1118
+ progress: cards.length > 0 ? (index + 1) / cards.length : 1,
1119
+ value: decision,
1120
+ meta: { cardId: card.id },
1121
+ });
1122
+ dragX.setValue(0);
1123
+ setDecisions((value) => [...value, { id: card.id, decision }]);
1124
+ setIndex((value) => value + 1);
1125
+ });
1126
+ };
1127
+ decideRef.current = decide;
1128
+
1129
+ const responder = useMemo(
1130
+ () =>
1131
+ PanResponder.create({
1132
+ onMoveShouldSetPanResponder: (_, gesture) => Math.abs(gesture.dx) > 4,
1133
+ onPanResponderMove: Animated.event([null, { dx: dragX }], { useNativeDriver: false }),
1134
+ onPanResponderRelease: (_, gesture) => {
1135
+ if (Math.abs(gesture.dx) >= 72) decideRef.current(gesture.dx > 0 ? "keep" : "skip");
1136
+ else Animated.spring(dragX, { toValue: 0, useNativeDriver: false }).start();
1137
+ },
1138
+ onPanResponderTerminate: () => {
1139
+ Animated.spring(dragX, { toValue: 0, useNativeDriver: false }).start();
1140
+ },
1141
+ }),
1142
+ [dragX],
1143
+ );
1144
+
1145
+ useEffect(() => () => dragX.stopAnimation(), [dragX]);
1146
+
1147
+ const rotate = dragX.interpolate({
1148
+ inputRange: [-220, 0, 220],
1149
+ outputRange: ["-9deg", "0deg", "9deg"],
1150
+ extrapolate: "clamp",
1151
+ });
1152
+ const kept = decisions.filter(({ decision }) => decision === "keep").length;
1153
+
1154
+ return (
1155
+ <View style={sh.content}>
1156
+ <Title text={spec.title} theme={theme} />
1157
+ <Body text={spec.body} theme={theme} />
1158
+ {current ? (
1159
+ <>
1160
+ <View style={deck.progressRow}>
1161
+ <Text style={[deck.progressText, { color: theme.secondaryColor }]}>
1162
+ {index + 1} / {cards.length}
1163
+ </Text>
1164
+ <Text style={[deck.progressText, { color: theme.accent }]}>{kept} kept</Text>
1165
+ </View>
1166
+ <Animated.View
1167
+ {...responder.panHandlers}
1168
+ style={[
1169
+ deck.card,
1170
+ { backgroundColor: theme.couponBg, borderColor: theme.couponBorder, transform: [{ translateX: dragX }, { rotate }] },
1171
+ ]}
1172
+ accessibilityLabel={`${current.title}. Swipe right to keep or left to skip`}
1173
+ >
1174
+ {current.image ? (
1175
+ <Image
1176
+ source={{ uri: current.image }}
1177
+ style={[
1178
+ deck.image,
1179
+ mediaHeight === undefined ? null : { height: mediaHeight },
1180
+ ] as any}
1181
+ resizeMode="cover"
1182
+ />
1183
+ ) : null}
1184
+ <Text style={[deck.title, { color: theme.titleColor }]}>{current.title}</Text>
1185
+ {current.body ? <Text style={[deck.body, { color: theme.bodyColor }]}>{current.body}</Text> : null}
1186
+ </Animated.View>
1187
+ <View style={deck.actions}>
1188
+ <SecondaryBtn label={skipLabel} onPress={() => decide("skip")} color={theme.secondaryColor} />
1189
+ <PrimaryBtn label={keepLabel} onPress={() => decide("keep")} accent={theme.accent} />
1190
+ </View>
1191
+ </>
1192
+ ) : (
1193
+ <View style={[deck.complete, { backgroundColor: theme.couponBg }]} accessibilityLiveRegion="polite">
1194
+ <Text style={[deck.completeTitle, { color: theme.titleColor }]}>
1195
+ {cards.length === 0 ? "Deck unavailable" : str(e.finalLabel) || "Deck complete"}
1196
+ </Text>
1197
+ <Text style={[deck.completeBody, { color: theme.bodyColor }]}>
1198
+ {cards.length === 0 ? "No cards were authored for this deck." : `You kept ${kept} of ${cards.length}`}
1199
+ </Text>
1200
+ </View>
1201
+ )}
1202
+ {!current ? <CtaArea {...props} /> : null}
1203
+ </View>
1204
+ );
1205
+ }
1206
+
1207
+ const deck: Record<string, ViewStyle & TextStyle> = {
1208
+ progressRow: { flexDirection: "row", justifyContent: "space-between", marginTop: 10 } as any,
1209
+ progressText: { fontSize: 11, fontWeight: "700" } as any,
1210
+ card: { minHeight: 220, borderWidth: 1.5, borderRadius: 18, padding: 16, marginVertical: 12, overflow: "hidden" } as any,
1211
+ image: { width: "100%", height: 132, borderRadius: 12, marginBottom: 12 } as any,
1212
+ title: { fontSize: 18, fontWeight: "800", textAlign: "center" } as any,
1213
+ body: { fontSize: 13, lineHeight: 18, textAlign: "center", marginTop: 5 } as any,
1214
+ actions: { flexDirection: "row", flexWrap: "wrap", gap: 12, alignItems: "center" } as any,
1215
+ complete: { borderRadius: 16, padding: 22, marginTop: 14, alignItems: "center" } as any,
1216
+ completeTitle: { fontSize: 18, fontWeight: "800" } as any,
1217
+ completeBody: { fontSize: 13, marginTop: 5 } as any,
1218
+ };
1219
+
1220
+ // ─── 3. Scratch Card ──────────────────────────────────────────────────────────
1221
+
1222
+ const SCRATCH_COLUMNS = 6;
1223
+ const SCRATCH_ROWS = 4;
1224
+ const SCRATCH_CELLS = SCRATCH_COLUMNS * SCRATCH_ROWS;
1225
+ const SCRATCH_REVEAL_AT = 0.55;
1226
+
1227
+ export function ScratchCardInApp(props: InAppBranchProps): React.ReactElement {
1228
+ const { spec, theme } = props;
1229
+ const e = ext(props);
1230
+ const mystery = variantOf(props) === "mystery_box";
1231
+ const [opened, setOpened] = useState(false);
1232
+ const [scratched, setScratched] = useState<number[]>([]);
1233
+ const scratchBounds = useRef({ width: 0, height: 0 });
1234
+ const reportedProgress = useRef(0);
1235
+ const outcomeShown = useRef(false);
1236
+ const boxMotion = useRef(new Animated.Value(0)).current;
1237
+ const reducedMotion = useReducedMotion();
1238
+ const [gameOutcome, setGameOutcome] = useState<InAppGameOutcome | null>(null);
1239
+ const [gameLoading, setGameLoading] = useState(false);
1240
+ const [gameError, setGameError] = useState("");
1241
+ const prize = gameResultLabel(gameOutcome) || "Reward revealed";
1242
+ const ctaIsPlay = /scratch|reveal|open/i.test(spec.cta?.label ?? "");
1243
+ const claimCta = spec.cta && !ctaIsPlay ? spec.cta : undefined;
1244
+ const rewardTitle = str(e.rewardTitle) || (mystery ? "Surprise!" : "You won!");
1245
+ const closed =
1246
+ str(e.closedMessage) ||
1247
+ str(e.revealLabel) ||
1248
+ (mystery ? "Tap the box to open it" : "Scratch to reveal your prize");
1249
+ const progress = scratchProgress(scratched, SCRATCH_CELLS);
1250
+ const revealed = mystery ? opened : progress >= SCRATCH_REVEAL_AT;
1251
+
1252
+ useEffect(() => () => boxMotion.stopAnimation(), [boxMotion]);
1253
+ useEffect(() => {
1254
+ if (
1255
+ !revealed ||
1256
+ !gameOutcome ||
1257
+ outcomeShown.current ||
1258
+ !props.onOutcome
1259
+ ) {
1260
+ return;
1261
+ }
1262
+ props.onOutcome({
1263
+ resultLabel: prize,
1264
+ fallback: inAppOutcomeTerminalForCta(props.spec.cta),
1265
+ });
1266
+ outcomeShown.current = true;
1267
+ }, [
1268
+ gameOutcome,
1269
+ prize,
1270
+ props.onOutcome,
1271
+ props.spec.cta,
1272
+ revealed,
1273
+ ]);
1274
+
1275
+ const resolveReward = async (): Promise<InAppGameOutcome | null> => {
1276
+ if (gameOutcome) return gameOutcome;
1277
+ if (gameLoading) return null;
1278
+ setGameLoading(true);
1279
+ setGameError("");
1280
+ props.onInteraction?.({
1281
+ mechanic: mystery ? "mystery_box" : "scratch_card",
1282
+ phase: "started",
1283
+ attempt: 1,
1284
+ });
1285
+ try {
1286
+ const outcome = await requireGameOutcome(props);
1287
+ setGameOutcome(outcome);
1288
+ return outcome;
1289
+ } catch (error) {
1290
+ setGameError(gameUnavailableText(error));
1291
+ return null;
1292
+ } finally {
1293
+ setGameLoading(false);
1294
+ }
1295
+ };
1296
+
1297
+ const scratchResponder = useMemo(() => {
1298
+ const mark = (event: GestureResponderEvent) => {
1299
+ if (!gameOutcome) return;
1300
+ const { locationX, locationY } = event.nativeEvent;
1301
+ const { width, height } = scratchBounds.current;
1302
+ const cell = scratchCellAt(
1303
+ locationX,
1304
+ locationY,
1305
+ width,
1306
+ height,
1307
+ SCRATCH_COLUMNS,
1308
+ SCRATCH_ROWS,
1309
+ );
1310
+ setScratched((current) => {
1311
+ const next = addScratchedCell(current, cell);
1312
+ const nextProgress = scratchProgress(next, SCRATCH_CELLS);
1313
+ const reportAt = nextProgress >= SCRATCH_REVEAL_AT
1314
+ ? 1
1315
+ : Math.min(0.5, Math.floor(nextProgress * 4) / 4);
1316
+ if (reportAt > reportedProgress.current) {
1317
+ reportedProgress.current = reportAt;
1318
+ props.onInteraction?.({
1319
+ mechanic: "scratch",
1320
+ phase: nextProgress >= SCRATCH_REVEAL_AT ? "resolved" : "progress",
1321
+ progress: nextProgress,
1322
+ result: nextProgress >= SCRATCH_REVEAL_AT ? prize : undefined,
1323
+ });
1324
+ }
1325
+ return next;
1326
+ });
1327
+ };
1328
+ return PanResponder.create({
1329
+ onStartShouldSetPanResponder: () => true,
1330
+ onMoveShouldSetPanResponder: () => true,
1331
+ onPanResponderGrant: mark,
1332
+ onPanResponderMove: mark,
1333
+ });
1334
+ }, [
1335
+ gameOutcome,
1336
+ prize,
1337
+ props.onInteraction,
1338
+ props.onOutcome,
1339
+ props.spec.cta,
1340
+ ]);
1341
+
1342
+ const revealScratchAccessibly = () => {
1343
+ if (!gameOutcome) {
1344
+ void resolveReward();
1345
+ return;
1346
+ }
1347
+ setScratched(Array.from({ length: SCRATCH_CELLS }, (_, index) => index));
1348
+ props.onInteraction?.({
1349
+ mechanic: "scratch",
1350
+ phase: "resolved",
1351
+ progress: 1,
1352
+ result: prize,
1353
+ });
1354
+ };
1355
+
1356
+ const openMystery = async () => {
1357
+ if (opened) return;
1358
+ const outcome = await resolveReward();
1359
+ if (!outcome) return;
1360
+ const duration = reducedMotion === false ? 90 : 0;
1361
+ Animated.sequence([
1362
+ Animated.timing(boxMotion, { toValue: 1, duration, useNativeDriver: true }),
1363
+ Animated.timing(boxMotion, { toValue: -1, duration, useNativeDriver: true }),
1364
+ Animated.timing(boxMotion, { toValue: 0, duration, useNativeDriver: true }),
1365
+ ]).start(({ finished }) => {
1366
+ if (!finished) return;
1367
+ setOpened(true);
1368
+ props.onInteraction?.({
1369
+ mechanic: "mystery_box",
1370
+ phase: "resolved",
1371
+ result: gameResultLabel(outcome) || "Reward revealed",
1372
+ });
1373
+ AccessibilityInfo.announceForAccessibility(
1374
+ `${rewardTitle} ${gameResultLabel(outcome) || "Reward revealed"}`,
1375
+ );
1376
+ });
1377
+ };
1378
+
1379
+ const boxRotate = boxMotion.interpolate({
1380
+ inputRange: [-1, 0, 1],
1381
+ outputRange: ["-7deg", "0deg", "7deg"],
1382
+ });
1383
+ return (
1384
+ <View style={sh.content}>
1385
+ <Title text={spec.title} theme={theme} />
1386
+ <Body text={spec.body} theme={theme} />
1387
+ {mystery ? (
1388
+ <Pressable
1389
+ style={[sc.boxStage, { backgroundColor: theme.couponBg }]}
1390
+ onPress={() => { void openMystery(); }}
1391
+ disabled={opened || gameLoading}
1392
+ accessibilityRole="button"
1393
+ accessibilityLabel={opened ? `${rewardTitle}: ${prize}` : closed}
1394
+ >
1395
+ <Animated.View style={[sc.box, { transform: [{ rotate: boxRotate }] }]}>
1396
+ <View
1397
+ style={[
1398
+ sc.boxLid,
1399
+ { backgroundColor: theme.accent },
1400
+ opened ? sc.boxLidOpen : null,
1401
+ ]}
1402
+ />
1403
+ <View style={[sc.boxBody, { backgroundColor: theme.accent }]}>
1404
+ <Text style={sc.boxEmoji}>{opened ? "✨" : "🎁"}</Text>
1405
+ </View>
1406
+ </Animated.View>
1407
+ <Text style={[sc.rewardTitle, { color: theme.titleColor }]}>
1408
+ {opened ? rewardTitle : gameLoading ? "Checking your result…" : gameError ? "Try opening again" : closed}
1409
+ </Text>
1410
+ {opened ? <Text style={[sc.prize, { color: theme.accent }]}>{prize}</Text> : null}
1411
+ </Pressable>
1412
+ ) : (
1413
+ <>
1414
+ <View style={[sc.scratch, { borderColor: theme.accent, backgroundColor: theme.couponBg }]}>
1415
+ <View
1416
+ accessibilityElementsHidden={!revealed}
1417
+ importantForAccessibility={revealed ? "auto" : "no-hide-descendants"}
1418
+ style={sc.rewardCopy}
1419
+ >
1420
+ <Text style={[sc.rewardTitle, { color: theme.titleColor }]}>{rewardTitle}</Text>
1421
+ <Text style={[sc.prize, { color: theme.accent }]}>{prize}</Text>
1422
+ {!revealed ? <Text style={sc.scratchHintMeasure}>{closed}</Text> : null}
1423
+ </View>
1424
+ {!revealed ? (
1425
+ <View
1426
+ style={sc.cover}
1427
+ onLayout={(event) => {
1428
+ scratchBounds.current = event.nativeEvent.layout;
1429
+ }}
1430
+ {...(gameOutcome ? scratchResponder.panHandlers : {})}
1431
+ accessibilityRole="button"
1432
+ accessibilityLabel={`${closed}. ${Math.round(progress * 100)} percent revealed`}
1433
+ accessibilityActions={[{ name: "activate", label: "Reveal reward" }]}
1434
+ onAccessibilityAction={revealScratchAccessibly}
1435
+ >
1436
+ {Array.from({ length: SCRATCH_CELLS }, (_, index) => (
1437
+ <View
1438
+ key={index}
1439
+ pointerEvents="none"
1440
+ style={[
1441
+ sc.coverCell,
1442
+ { width: `${100 / SCRATCH_COLUMNS}%` as any },
1443
+ scratched.includes(index) ? sc.coverCellCleared : null,
1444
+ ]}
1445
+ />
1446
+ ))}
1447
+ <View pointerEvents="none" style={sc.scratchHintWrap}>
1448
+ <Text style={sc.scratchHint}>{closed}</Text>
1449
+ </View>
1450
+ </View>
1451
+ ) : null}
1452
+ </View>
1453
+ <View style={[sc.progressTrack, { backgroundColor: theme.couponBg }]}>
1454
+ <View style={[sc.progressFill, { width: `${progress * 100}%` as any, backgroundColor: theme.accent }]} />
1455
+ </View>
1456
+ <Text style={[sc.progressLabel, { color: theme.secondaryColor }]}>
1457
+ {revealed ? "Reward revealed" : `${Math.round(progress * 100)}% scratched`}
1458
+ </Text>
1459
+ </>
1460
+ )}
1461
+ {revealed ? (
1462
+ <PrimaryBtn
1463
+ label={str(e.claimLabel) || claimCta?.label || "DONE"}
1464
+ accent={theme.accent}
1465
+ onPress={() => {
1466
+ props.onInteraction?.({
1467
+ mechanic: mystery ? "mystery_box" : "scratch",
1468
+ phase: "claimed",
1469
+ result: prize,
1470
+ });
1471
+ if (claimCta?.action === "dismiss") props.onDismiss();
1472
+ else if (claimCta) props.onCta(claimCta);
1473
+ else props.onDismiss();
1474
+ }}
1475
+ />
1476
+ ) : null}
1477
+ {!mystery && !gameOutcome ? (
1478
+ <PrimaryBtn
1479
+ label={gameLoading ? "CHECKING…" : gameError ? "RETRY" : str(e.startLabel) || "START"}
1480
+ accent={theme.accent}
1481
+ onPress={() => { void resolveReward(); }}
1482
+ disabled={gameLoading}
1483
+ />
1484
+ ) : null}
1485
+ {gameError ? <Text style={[sc.legal, { color: theme.secondaryColor }]}>{gameError}</Text> : null}
1486
+ {str(e.legalText) ? <Text style={[sc.legal, { color: theme.secondaryColor }]}>{str(e.legalText)}</Text> : null}
1487
+ </View>
1488
+ );
1489
+ }
1490
+
1491
+ const sc: Record<string, ViewStyle & TextStyle> = {
1492
+ scratch: {
1493
+ marginVertical: 14,
1494
+ borderRadius: 14,
1495
+ borderWidth: 2,
1496
+ borderStyle: "dashed",
1497
+ minHeight: 132,
1498
+ justifyContent: "center",
1499
+ alignItems: "center",
1500
+ overflow: "hidden",
1501
+ position: "relative",
1502
+ } as any,
1503
+ prize: { fontSize: 28, fontWeight: "900", letterSpacing: 1, textAlign: "center" } as any,
1504
+ rewardCopy: {
1505
+ alignItems: "center",
1506
+ alignSelf: "stretch",
1507
+ justifyContent: "center",
1508
+ paddingHorizontal: 16,
1509
+ paddingVertical: 24,
1510
+ } as any,
1511
+ rewardTitle: { fontSize: 13, fontWeight: "800", marginBottom: 6, textAlign: "center" } as any,
1512
+ cover: {
1513
+ position: "absolute",
1514
+ top: 0,
1515
+ right: 0,
1516
+ bottom: 0,
1517
+ left: 0,
1518
+ flexDirection: "row",
1519
+ flexWrap: "wrap",
1520
+ alignContent: "stretch",
1521
+ backgroundColor: "#858B96",
1522
+ } as any,
1523
+ coverCell: {
1524
+ height: `${100 / SCRATCH_ROWS}%`,
1525
+ backgroundColor: "#9299A5",
1526
+ borderWidth: 0.5,
1527
+ borderColor: "rgba(255,255,255,0.18)",
1528
+ } as any,
1529
+ coverCellCleared: { opacity: 0 } as any,
1530
+ scratchHintWrap: {
1531
+ position: "absolute",
1532
+ alignItems: "center",
1533
+ bottom: 0,
1534
+ justifyContent: "center",
1535
+ left: 16,
1536
+ right: 16,
1537
+ top: 0,
1538
+ } as any,
1539
+ scratchHint: {
1540
+ fontSize: 13,
1541
+ color: "#fff",
1542
+ fontWeight: "600",
1543
+ textAlign: "center",
1544
+ } as any,
1545
+ scratchHintMeasure: {
1546
+ fontSize: 13,
1547
+ fontWeight: "600",
1548
+ marginTop: 8,
1549
+ opacity: 0,
1550
+ textAlign: "center",
1551
+ } as any,
1552
+ progressTrack: { height: 7, borderRadius: 4, overflow: "hidden" } as any,
1553
+ progressFill: { height: "100%", borderRadius: 4 } as any,
1554
+ progressLabel: { fontSize: 11, textAlign: "center", marginTop: 5 } as any,
1555
+ boxStage: { borderRadius: 16, minHeight: 190, marginVertical: 14, alignItems: "center", justifyContent: "center", padding: 16 } as any,
1556
+ box: { width: 94, height: 94, alignItems: "center", justifyContent: "flex-end" } as any,
1557
+ boxLid: { width: 98, height: 22, borderRadius: 6, marginBottom: -4, zIndex: 1 } as any,
1558
+ boxLidOpen: { transform: [{ translateY: -12 }, { rotate: "-10deg" }] } as any,
1559
+ boxBody: { width: 82, height: 70, borderRadius: 8, alignItems: "center", justifyContent: "center" } as any,
1560
+ boxEmoji: { fontSize: 36 } as any,
1561
+ legal: { fontSize: 10, lineHeight: 14, marginTop: 10, textAlign: "center" } as any,
1562
+ };
1563
+
1564
+ // ─── 4. Slot Reward ───────────────────────────────────────────────────────────
1565
+
1566
+ const DEFAULT_REELS = [
1567
+ ["🍒", "⭐", "💎", "🍋", "🎰"],
1568
+ ["⭐", "🍒", "🍋", "💎", "🎰"],
1569
+ ["💎", "🍋", "🍒", "⭐", "🎰"],
1570
+ ];
1571
+
1572
+ export function SlotRewardInApp(props: InAppBranchProps): React.ReactElement {
1573
+ const { spec, theme } = props;
1574
+ const e = ext(props);
1575
+ const configuredReels = arr(e.reels)
1576
+ .map((reel) =>
1577
+ arr(record(reel).symbols ?? reel)
1578
+ .map(labelOf)
1579
+ .filter(Boolean),
1580
+ )
1581
+ .filter((reel) => reel.length > 0);
1582
+ const slotItems = arr(e.slotItems)
1583
+ .map((item) => {
1584
+ const value = record(item);
1585
+ return str(value.emoji ?? value.symbol ?? value.label ?? item);
1586
+ })
1587
+ .filter(Boolean);
1588
+ const reels =
1589
+ configuredReels.length > 0
1590
+ ? configuredReels
1591
+ : slotItems.length > 0
1592
+ ? [slotItems, slotItems, slotItems]
1593
+ : DEFAULT_REELS;
1594
+ const boundedReels = reels.slice(0, 3).map((reel) => reel.slice(0, 12));
1595
+
1596
+ const [phase, setPhase] = useState<"idle" | "resolving" | "spinning" | "result">("idle");
1597
+ const [results, setResults] = useState<string[]>([]);
1598
+ const [gameOutcome, setGameOutcome] = useState<InAppGameOutcome | null>(null);
1599
+ const [gameError, setGameError] = useState("");
1600
+ const [frame, setFrame] = useState(0);
1601
+ const [attemptsUsed, setAttemptsUsed] = useState(0);
1602
+ const reducedMotion = useReducedMotion();
1603
+ const resultMessage = gameResultLabel(gameOutcome);
1604
+ // One UI attempt maps to one idempotent backend attempt. Retry reuses its id.
1605
+ const maxAttempts = 1;
1606
+ const spinTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
1607
+ const animsRef = useRef<Animated.Value[]>([]);
1608
+ while (animsRef.current.length < boundedReels.length)
1609
+ animsRef.current.push(new Animated.Value(0));
1610
+ const anims = animsRef.current.slice(0, boundedReels.length);
1611
+
1612
+ useEffect(() => () => {
1613
+ if (spinTimerRef.current) clearInterval(spinTimerRef.current);
1614
+ animsRef.current.forEach((animation) => animation.stopAnimation());
1615
+ }, []);
1616
+
1617
+ const pull = async () => {
1618
+ if (phase === "resolving" || phase === "spinning" || attemptsUsed >= maxAttempts) return;
1619
+ const attempt = attemptsUsed + 1;
1620
+ setGameError("");
1621
+ setPhase("resolving");
1622
+ setResults([]);
1623
+ props.onInteraction?.({ mechanic: "slot_reward", phase: "started", attempt });
1624
+ let remoteOutcome: InAppGameOutcome;
1625
+ try {
1626
+ remoteOutcome = await requireGameOutcome(props, attempt);
1627
+ } catch (error) {
1628
+ setGameError(gameUnavailableText(error));
1629
+ setPhase("idle");
1630
+ return;
1631
+ }
1632
+ const outcome = resolveSlotOutcome(boundedReels, remoteOutcome.symbols ?? []);
1633
+ if (!outcome.authored) {
1634
+ setGameError("The backend symbols do not match the configured reels.");
1635
+ setPhase("idle");
1636
+ return;
1637
+ }
1638
+ setAttemptsUsed(attempt);
1639
+ setGameOutcome(remoteOutcome);
1640
+ const remoteResultMessage = gameResultLabel(remoteOutcome);
1641
+ setPhase("spinning");
1642
+ if (reducedMotion === false) {
1643
+ spinTimerRef.current = setInterval(() => setFrame((value) => value + 1), 85);
1644
+ }
1645
+ Animated.stagger(
1646
+ reducedMotion === false ? 150 : 0,
1647
+ anims.map((a, i) =>
1648
+ Animated.sequence([
1649
+ Animated.timing(a, {
1650
+ toValue: 1,
1651
+ duration: reducedMotion === false ? 600 + i * 100 : 0,
1652
+ easing: Easing.out(Easing.back(2)),
1653
+ useNativeDriver: true,
1654
+ }),
1655
+ Animated.timing(a, {
1656
+ toValue: 0,
1657
+ duration: 0,
1658
+ useNativeDriver: true,
1659
+ }),
1660
+ ]),
1661
+ ),
1662
+ ).start(({ finished }) => {
1663
+ if (!finished) return;
1664
+ if (spinTimerRef.current) clearInterval(spinTimerRef.current);
1665
+ spinTimerRef.current = null;
1666
+ setResults(outcome.symbols);
1667
+ setPhase("result");
1668
+ props.onInteraction?.({
1669
+ mechanic: "slot_reward",
1670
+ phase: "resolved",
1671
+ attempt,
1672
+ result: remoteResultMessage || undefined,
1673
+ meta: { backendResolved: true },
1674
+ });
1675
+ AccessibilityInfo.announceForAccessibility(remoteResultMessage || "Spin complete");
1676
+ presentTerminalOutcome(props, remoteResultMessage);
1677
+ });
1678
+ };
1679
+
1680
+ const playLabel = str(e.ctaLabel) || spec.cta?.label || "PULL";
1681
+ const ctaIsPlay = /spin|pull/i.test(spec.cta?.label ?? "");
1682
+ const finish = () => {
1683
+ props.onInteraction?.({
1684
+ mechanic: "slot_reward",
1685
+ phase: "completed",
1686
+ attempt: attemptsUsed,
1687
+ result: resultMessage || undefined,
1688
+ });
1689
+ if (spec.cta && !ctaIsPlay && spec.cta.action !== "dismiss") props.onCta(spec.cta);
1690
+ else props.onDismiss();
1691
+ };
1692
+
1693
+ return (
1694
+ <View style={sh.content}>
1695
+ <Title text={spec.title} theme={theme} />
1696
+ <Body text={spec.body} theme={theme} />
1697
+ <View style={sl.machine}>
1698
+ {boundedReels.map((reel, i) => {
1699
+ const displayEmoji = results[i] ?? reel[(frame + i) % reel.length] ?? "★";
1700
+ const scale = anims[i].interpolate({
1701
+ inputRange: [0, 0.5, 1],
1702
+ outputRange: [1, 1.4, 1],
1703
+ });
1704
+ const translateY = anims[i].interpolate({
1705
+ inputRange: [0, 0.5, 1],
1706
+ outputRange: [0, -18, 0],
1707
+ });
1708
+ return (
1709
+ <View
1710
+ key={i}
1711
+ style={[
1712
+ sl.reel,
1713
+ {
1714
+ borderColor: theme.accent + "55",
1715
+ backgroundColor: theme.couponBg,
1716
+ },
1717
+ ]}
1718
+ >
1719
+ <Animated.Text style={[sl.reelEmoji, { transform: [{ scale }, { translateY }] }]}>
1720
+ {displayEmoji}
1721
+ </Animated.Text>
1722
+ </View>
1723
+ );
1724
+ })}
1725
+ </View>
1726
+ <Text style={[sl.attempts, { color: theme.secondaryColor }]}>
1727
+ Attempt {Math.min(attemptsUsed + (phase === "idle" ? 1 : 0), maxAttempts)} of {maxAttempts}
1728
+ </Text>
1729
+ {phase === "result" ? (
1730
+ <View style={[sl.win, { backgroundColor: resultMessage ? theme.accent : theme.couponBg }]}>
1731
+ <Text style={[sl.winText, !resultMessage && { color: theme.titleColor }]}>
1732
+ {resultMessage || "Spin complete"}
1733
+ </Text>
1734
+ </View>
1735
+ ) : null}
1736
+ {phase === "idle" && gameError ? (
1737
+ <Text style={[sl.unavailable, { color: theme.secondaryColor }]}>
1738
+ {gameError}
1739
+ </Text>
1740
+ ) : null}
1741
+ {phase === "result" ? (
1742
+ <PrimaryBtn label={ctaIsPlay ? "DONE" : spec.cta?.label || "DONE"} accent={theme.accent} onPress={finish} />
1743
+ ) : (
1744
+ <PrimaryBtn
1745
+ label={phase === "resolving" ? "CHECKING…" : phase === "spinning" ? "SPINNING…" : gameError ? "RETRY" : playLabel}
1746
+ accent={theme.accent}
1747
+ onPress={() => { void pull(); }}
1748
+ disabled={phase === "resolving" || phase === "spinning"}
1749
+ />
1750
+ )}
1751
+ </View>
1752
+ );
1753
+ }
1754
+
1755
+ const sl: Record<string, ViewStyle & TextStyle> = {
1756
+ machine: {
1757
+ alignSelf: "center",
1758
+ flexDirection: "row",
1759
+ gap: 10,
1760
+ justifyContent: "center",
1761
+ maxWidth: 230,
1762
+ marginVertical: 16,
1763
+ width: "100%",
1764
+ } as any,
1765
+ reel: {
1766
+ flex: 1,
1767
+ height: 80,
1768
+ maxWidth: 70,
1769
+ minWidth: 0,
1770
+ borderRadius: 12,
1771
+ borderWidth: 1.5,
1772
+ justifyContent: "center",
1773
+ alignItems: "center",
1774
+ } as any,
1775
+ reelEmoji: { fontSize: 36 } as any,
1776
+ win: {
1777
+ borderRadius: 10,
1778
+ paddingVertical: 8,
1779
+ paddingHorizontal: 14,
1780
+ alignSelf: "center",
1781
+ marginBottom: 10,
1782
+ } as any,
1783
+ winText: { color: "#fff", fontWeight: "800", fontSize: 15 } as any,
1784
+ attempts: { fontSize: 11, fontWeight: "600", textAlign: "center", marginBottom: 8 } as any,
1785
+ unavailable: { fontSize: 11, lineHeight: 15, textAlign: "center", marginBottom: 8 } as any,
1786
+ };
1787
+
1788
+ // ─── 5. Poll Card ─────────────────────────────────────────────────────────────
1789
+
1790
+ export function PollCardInApp(props: InAppBranchProps): React.ReactElement {
1791
+ const { spec, theme } = props;
1792
+ const e = ext(props);
1793
+ const variantKey = variantOf(props);
1794
+ const chipMode =
1795
+ variantKey === "preferences_picker" || variantKey === "product_finder";
1796
+ const quizMode = variantKey === "quiz_card";
1797
+ const questions = arr(e.questions).slice(0, 20);
1798
+ const [questionIndex, setQuestionIndex] = useState(0);
1799
+ const [answerPath, setAnswerPath] = useState<string[]>([]);
1800
+ const questionSpec = record(questions[questionIndex]);
1801
+ const question =
1802
+ str(questionSpec.prompt ?? questionSpec.question ?? questionSpec.title) ||
1803
+ str(e.question ?? e.promptText) ||
1804
+ spec.title ||
1805
+ "What do you think?";
1806
+ const categoryOptions: unknown[] = [];
1807
+ for (const category of arr(e.categories).slice(0, 20)) {
1808
+ const value = record(category);
1809
+ categoryOptions.push(...arr(value.options ?? value.items ?? value.choices).slice(0, 20));
1810
+ if (categoryOptions.length >= 20) break;
1811
+ }
1812
+ const optionSources = [
1813
+ questionSpec.choices,
1814
+ questionSpec.options,
1815
+ questionSpec.answers,
1816
+ e.options,
1817
+ e.answers,
1818
+ e.preferenceOptions,
1819
+ e.productList,
1820
+ e.itemList,
1821
+ categoryOptions,
1822
+ ];
1823
+ const rawOptions = arr(
1824
+ optionSources.find((value) => Array.isArray(value) && value.length > 0) ??
1825
+ [],
1826
+ ).slice(0, 20);
1827
+ const scaleMax = boundedInt(questionSpec.scaleMax, 5, 2, 10);
1828
+ const options =
1829
+ rawOptions.length > 0
1830
+ ? rawOptions.map((option, index) => labelOf(option) || `Option ${index + 1}`)
1831
+ : questionSpec.kind === "scale"
1832
+ ? Array.from({ length: scaleMax }, (_, i) => String(i + 1))
1833
+ : [];
1834
+ const showResults = e.showResults === true;
1835
+ const [selected, setSelected] = useState<number[]>([]);
1836
+ const [answerLocked, setAnswerLocked] = useState(false);
1837
+ const [score, setScore] = useState(0);
1838
+ const [gradedCount, setGradedCount] = useState(0);
1839
+ const [quizAnswers, setQuizAnswers] = useState<number[][]>([]);
1840
+ const [quizOutcome, setQuizOutcome] = useState<InAppGameOutcome | null>(null);
1841
+ const [quizLoading, setQuizLoading] = useState(false);
1842
+ const [quizError, setQuizError] = useState("");
1843
+ const quizSubmission = useRef<Record<string, unknown> | null>(null);
1844
+ const allowMultiple =
1845
+ questionSpec.kind === "multi" ||
1846
+ e.allowMultiple === true ||
1847
+ (quizMode && Array.isArray(questionSpec.correctAnswer ?? e.correctAnswer)) ||
1848
+ (variantKey === "preferences_picker" && e.multiSelect !== false);
1849
+ const hasNext = questionIndex + 1 < questions.length;
1850
+ const answered = selected.length > 0;
1851
+ const correctAnswer = questionSpec.correctAnswer ?? e.correctAnswer;
1852
+ const correctIndexes = (Array.isArray(correctAnswer) ? correctAnswer : [correctAnswer])
1853
+ .map((answer) =>
1854
+ typeof answer === "number"
1855
+ ? Math.floor(answer)
1856
+ : options.findIndex((option) => option === labelOf(answer)),
1857
+ )
1858
+ .filter((index) => index >= 0 && index < options.length);
1859
+ const quizGraded = quizMode && correctIndexes.length > 0;
1860
+ const quizCorrect = quizGraded && gradeGameAnswer(selected, correctIndexes);
1861
+ const explanation =
1862
+ firstText(questionSpec, ["explanation"]) || str(e.explanation);
1863
+ const selectedLabel = options[selected[0] ?? -1] ?? "";
1864
+ const resultMapping = record(e.resultMapping);
1865
+ const currentAnswerPath = [...answerPath, selectedLabel].filter(Boolean);
1866
+ const mappedValue =
1867
+ resultMapping[currentAnswerPath.join(" > ")] ??
1868
+ resultMapping[currentAnswerPath.join("|")] ??
1869
+ resultMapping[selectedLabel] ??
1870
+ arr(e.productList ?? props.spec.assets?.products)[selected[0] ?? -1];
1871
+ const mappedRecord = record(mappedValue);
1872
+ const mappedResult = labelOf(mappedValue);
1873
+ const mappedBody = firstText(mappedRecord, ["description", "body", "reason", "message"]);
1874
+ const mappedPrice = firstText(mappedRecord, ["price", "value", "discount"]);
1875
+ const mappedImage = mediaUri(mappedRecord.image ?? mappedRecord.imageUrl ?? mappedRecord.thumbnail);
1876
+
1877
+ const toggle = (index: number) => {
1878
+ if (quizMode && answerLocked) return;
1879
+ const next = toggleGameSelection(selected, index, allowMultiple);
1880
+ setSelected(next);
1881
+ if (!quizMode) {
1882
+ const labels = next.map((value) => options[value]).filter(Boolean);
1883
+ props.onInteraction?.({
1884
+ mechanic: variantKey || "question_flow",
1885
+ phase: "decision",
1886
+ value: labels[0],
1887
+ meta: { questionIndex, selected: next, labels },
1888
+ });
1889
+ }
1890
+ };
1891
+
1892
+ const submitQuiz = async (input: Record<string, unknown>): Promise<void> => {
1893
+ setQuizLoading(true);
1894
+ setQuizError("");
1895
+ quizSubmission.current = input;
1896
+ try {
1897
+ const outcome = await requireGameOutcome(props, 1, input);
1898
+ setQuizOutcome(outcome);
1899
+ props.onInteraction?.({
1900
+ mechanic: "quiz_card",
1901
+ phase: "completed",
1902
+ result: gameResultLabel(outcome) || outcome.id,
1903
+ meta: { backendResolved: true },
1904
+ });
1905
+ } catch (error) {
1906
+ setQuizError(gameUnavailableText(error));
1907
+ } finally {
1908
+ setQuizLoading(false);
1909
+ }
1910
+ };
1911
+ const checkQuizAnswer = () => {
1912
+ if (!answered || answerLocked) return;
1913
+ setAnswerLocked(true);
1914
+ const nextGradedCount = gradedCount + (quizGraded ? 1 : 0);
1915
+ const nextScore = scoreGameAnswer(score, quizCorrect, answerLocked, quizTotal);
1916
+ const nextAnswers = [...quizAnswers, [...selected]];
1917
+ if (quizGraded) setGradedCount(nextGradedCount);
1918
+ setScore(nextScore);
1919
+ setQuizAnswers(nextAnswers);
1920
+ props.onInteraction?.({
1921
+ mechanic: "quiz_card",
1922
+ phase: "resolved",
1923
+ progress: Math.min(1, (questionIndex + 1) / Math.max(1, questions.length)),
1924
+ value: quizGraded ? quizCorrect : undefined,
1925
+ meta: { questionIndex, selected },
1926
+ });
1927
+ if (!hasNext) {
1928
+ void submitQuiz({
1929
+ answers: nextAnswers,
1930
+ score: nextScore,
1931
+ gradedQuestions: nextGradedCount,
1932
+ totalQuestions: quizTotal,
1933
+ });
1934
+ }
1935
+ };
1936
+ const advanceQuiz = () => {
1937
+ setQuestionIndex((current) => current + 1);
1938
+ setSelected([]);
1939
+ setAnswerLocked(false);
1940
+ };
1941
+ const quizTotal = Math.max(1, questions.length);
1942
+ const reward = gameResultLabel(quizOutcome);
1943
+
1944
+ return (
1945
+ <View style={sh.content}>
1946
+ <Text style={[sh.title, { color: theme.titleColor }]}>{question}</Text>
1947
+ <Body text={spec.body} theme={theme} />
1948
+ {quizMode ? (
1949
+ <View style={po.quizProgress}>
1950
+ {Array.from({ length: quizTotal }, (_, index) => (
1951
+ <View
1952
+ key={index}
1953
+ style={[
1954
+ po.quizStep,
1955
+ { backgroundColor: index <= questionIndex ? theme.accent : theme.couponBg },
1956
+ ]}
1957
+ />
1958
+ ))}
1959
+ </View>
1960
+ ) : null}
1961
+ <View style={[po.options, chipMode && po.chips]}>
1962
+ {options.map((opt, i) => {
1963
+ const active = selected.includes(i);
1964
+ const correct = quizMode && answerLocked && correctIndexes.includes(i);
1965
+ const wrong = quizMode && answerLocked && active && !correct;
1966
+ const configuredPercent = num(record(rawOptions[i]).percent, -1);
1967
+ return (
1968
+ <Pressable
1969
+ key={i}
1970
+ style={[
1971
+ chipMode ? po.chip : po.option,
1972
+ {
1973
+ borderColor: correct ? "#22A06B" : wrong ? "#D92D20" : active ? theme.accent : "#E4E7EC",
1974
+ backgroundColor: correct || active ? theme.couponBg : "#FAFAFA",
1975
+ },
1976
+ ]}
1977
+ onPress={() => toggle(i)}
1978
+ disabled={quizMode && answerLocked}
1979
+ accessibilityRole={allowMultiple ? "checkbox" : "radio"}
1980
+ accessibilityState={
1981
+ allowMultiple ? { checked: active } : { selected: active }
1982
+ }
1983
+ accessibilityLabel={opt}
1984
+ >
1985
+ {!chipMode ? (
1986
+ <View
1987
+ style={[
1988
+ po.radio,
1989
+ {
1990
+ borderColor: correct ? "#22A06B" : wrong ? "#D92D20" : active ? theme.accent : "#9CA3AF",
1991
+ backgroundColor: correct ? "#22A06B" : wrong ? "#D92D20" : active ? theme.accent : "transparent",
1992
+ },
1993
+ ]}
1994
+ />
1995
+ ) : null}
1996
+ <Text
1997
+ style={[
1998
+ chipMode ? po.chipLabel : po.optLabel,
1999
+ { color: active ? theme.accent : theme.titleColor },
2000
+ ]}
2001
+ >
2002
+ {opt}
2003
+ </Text>
2004
+ {showResults && answered && configuredPercent >= 0 ? (
2005
+ <Text style={[po.pct, { color: theme.secondaryColor }]}>
2006
+ {configuredPercent}%
2007
+ </Text>
2008
+ ) : null}
2009
+ </Pressable>
2010
+ );
2011
+ })}
2012
+ </View>
2013
+ {quizMode && answerLocked && correctIndexes.length > 0 ? (
2014
+ <View style={[po.result, { backgroundColor: theme.couponBg }]}>
2015
+ <Text style={[po.resultTitle, { color: theme.titleColor }]}>
2016
+ {quizCorrect ? "Correct" : "Not quite"}
2017
+ </Text>
2018
+ {explanation ? (
2019
+ <Text style={[po.resultBody, { color: theme.bodyColor }]}>
2020
+ {explanation}
2021
+ </Text>
2022
+ ) : null}
2023
+ </View>
2024
+ ) : null}
2025
+ {quizMode && answerLocked && !quizGraded ? (
2026
+ <View style={[po.result, { backgroundColor: theme.couponBg }]}>
2027
+ <Text style={[po.resultTitle, { color: theme.titleColor }]}>Answer recorded</Text>
2028
+ </View>
2029
+ ) : null}
2030
+ {variantKey === "product_finder" && answered && !hasNext && mappedResult ? (
2031
+ <View style={[po.finderResult, { backgroundColor: theme.couponBg }]}>
2032
+ {mappedImage ? <Image source={{ uri: mappedImage }} resizeMode="cover" style={po.finderImage as any} /> : null}
2033
+ <View style={po.finderCopy}>
2034
+ <Text style={[po.resultTitle, { color: theme.titleColor }]}>{mappedResult}</Text>
2035
+ {mappedBody ? <Text style={[po.resultBody, { color: theme.bodyColor }]}>{mappedBody}</Text> : null}
2036
+ {mappedPrice ? <Text style={[po.finderPrice, { color: theme.accent }]}>{mappedPrice}</Text> : null}
2037
+ </View>
2038
+ </View>
2039
+ ) : null}
2040
+ {quizMode && answered && !answerLocked ? (
2041
+ <View style={sh.ctaRow}>
2042
+ <PrimaryBtn label={str(e.checkLabel) || "CHECK ANSWER"} accent={theme.accent} onPress={checkQuizAnswer} />
2043
+ </View>
2044
+ ) : quizMode && answerLocked ? (
2045
+ hasNext ? (
2046
+ <View style={sh.ctaRow}>
2047
+ <PrimaryBtn
2048
+ label={str(e.nextLabel) || "Next"}
2049
+ accent={theme.accent}
2050
+ onPress={advanceQuiz}
2051
+ />
2052
+ </View>
2053
+ ) : (
2054
+ <>
2055
+ <View style={[po.score, { backgroundColor: theme.couponBg }]} accessibilityLiveRegion="polite">
2056
+ <Text style={[po.scoreTitle, { color: theme.titleColor }]}>
2057
+ {gradedCount > 0 ? `Score ${score} / ${gradedCount}` : "Responses recorded"}
2058
+ </Text>
2059
+ {quizLoading ? <Text style={[po.scoreReward, { color: theme.bodyColor }]}>Confirming your result…</Text> : null}
2060
+ {quizError ? <Text style={[po.scoreReward, { color: theme.bodyColor }]}>{quizError}</Text> : null}
2061
+ {reward ? <Text style={[po.scoreReward, { color: theme.accent }]}>{reward}</Text> : null}
2062
+ </View>
2063
+ {quizError && !quizLoading ? (
2064
+ <PrimaryBtn label="RETRY" accent={theme.accent} onPress={() => {
2065
+ if (quizSubmission.current) void submitQuiz(quizSubmission.current);
2066
+ }} />
2067
+ ) : quizOutcome && (spec.cta || spec.secondaryCta) ? <CtaArea {...props} /> : quizOutcome ? (
2068
+ <PrimaryBtn
2069
+ label="DONE"
2070
+ accent={theme.accent}
2071
+ onPress={() => {
2072
+ if (!presentTerminalOutcome(props, reward)) props.onDismiss();
2073
+ }}
2074
+ />
2075
+ ) : null}
2076
+ </>
2077
+ )
2078
+ ) : answered ? (
2079
+ hasNext ? (
2080
+ <View style={sh.ctaRow}>
2081
+ <PrimaryBtn label={str(e.nextLabel) || "Next"} accent={theme.accent} onPress={() => {
2082
+ if (variantKey === "product_finder" && selectedLabel) {
2083
+ setAnswerPath((current) => [...current, selectedLabel]);
2084
+ }
2085
+ setQuestionIndex((current) => current + 1);
2086
+ setSelected([]);
2087
+ }} />
2088
+ </View>
2089
+ ) : <CtaArea {...props} beforePrimary={() => props.onInteraction?.({
2090
+ mechanic: variantKey || "question_flow",
2091
+ phase: "resolved",
2092
+ value: selectedLabel,
2093
+ meta: {
2094
+ questionIndex,
2095
+ selected,
2096
+ labels: selected.map((value) => options[value]).filter(Boolean),
2097
+ ...(variantKey === "product_finder" && mappedResult
2098
+ ? { result: { title: mappedResult, body: mappedBody, price: mappedPrice, image: mappedImage } }
2099
+ : {}),
2100
+ },
2101
+ })} />
2102
+ ) : null}
2103
+ </View>
2104
+ );
2105
+ }
2106
+
2107
+ const po: Record<string, ViewStyle & TextStyle> = {
2108
+ options: { marginVertical: 8 } as any,
2109
+ chips: { flexDirection: "row", flexWrap: "wrap", gap: 8 } as any,
2110
+ option: {
2111
+ flexDirection: "row",
2112
+ alignItems: "center",
2113
+ gap: 10,
2114
+ borderWidth: 1.5,
2115
+ borderRadius: 10,
2116
+ padding: 10,
2117
+ marginBottom: 8,
2118
+ } as any,
2119
+ chip: {
2120
+ borderWidth: 1.5,
2121
+ borderRadius: 999,
2122
+ maxWidth: "100%",
2123
+ paddingVertical: 9,
2124
+ paddingHorizontal: 14,
2125
+ } as any,
2126
+ radio: { width: 18, height: 18, borderRadius: 9, borderWidth: 2 } as any,
2127
+ optLabel: { flex: 1, fontSize: 13, fontWeight: "500" } as any,
2128
+ chipLabel: { flexShrink: 1, fontSize: 13, fontWeight: "600" } as any,
2129
+ pct: { fontSize: 12, fontWeight: "600" } as any,
2130
+ result: { borderRadius: 10, padding: 10, marginTop: 4 } as any,
2131
+ resultTitle: { fontSize: 13, fontWeight: "800" } as any,
2132
+ resultBody: { fontSize: 12, lineHeight: 17, marginTop: 2 } as any,
2133
+ quizProgress: { flexDirection: "row", gap: 5, marginVertical: 10 } as any,
2134
+ quizStep: { flex: 1, height: 5, borderRadius: 3 } as any,
2135
+ score: { borderRadius: 12, padding: 14, marginTop: 10, alignItems: "center" } as any,
2136
+ scoreTitle: { fontSize: 18, fontWeight: "900" } as any,
2137
+ scoreReward: { fontSize: 13, fontWeight: "700", marginTop: 4 } as any,
2138
+ finderResult: { flexDirection: "row", gap: 12, borderRadius: 12, padding: 12, marginTop: 8, alignItems: "center" } as any,
2139
+ finderImage: { width: 72, height: 72, borderRadius: 10 } as any,
2140
+ finderCopy: { flex: 1 } as any,
2141
+ finderPrice: { fontSize: 13, fontWeight: "800", marginTop: 4 } as any,
2142
+ };
2143
+
2144
+ // ─── 6. NPS Prompt ────────────────────────────────────────────────────────────
2145
+
2146
+ export function NpsPromptInApp(props: InAppBranchProps): React.ReactElement {
2147
+ const { spec, theme } = props;
2148
+ const e = ext(props);
2149
+ const question =
2150
+ str(e.question) || spec.title || "How likely are you to recommend us?";
2151
+ const scaleMax = boundedInt(e.scaleMax, 10, 1, 10);
2152
+ const scaleLabels = arr(e.scaleLabels).map(str);
2153
+ const [selected, setSelected] = useState<number | null>(null);
2154
+ const [feedback, setFeedback] = useState("");
2155
+ const feedbackEnabled = e.feedbackArea === true || typeof e.feedbackArea === "string";
2156
+ const emitNps = (): void => props.onInteraction?.({
2157
+ mechanic: "nps_prompt",
2158
+ phase: "resolved",
2159
+ value: selected ?? undefined,
2160
+ result: feedback.trim() || undefined,
2161
+ meta: feedback.trim() ? { feedback: feedback.trim() } : undefined,
2162
+ });
2163
+
2164
+ return (
2165
+ <View style={sh.content}>
2166
+ <Text style={[sh.title, { color: theme.titleColor }]}>{question}</Text>
2167
+ <Body text={spec.body} theme={theme} />
2168
+ <ScrollView
2169
+ horizontal
2170
+ showsHorizontalScrollIndicator={false}
2171
+ style={{ marginVertical: 12 }}
2172
+ contentContainerStyle={nps.row}
2173
+ >
2174
+ {Array.from({ length: scaleMax + 1 }, (_, i) => (
2175
+ <Pressable
2176
+ key={i}
2177
+ style={[
2178
+ nps.cell,
2179
+ {
2180
+ borderColor: selected === i ? theme.accent : "#E4E7EC",
2181
+ backgroundColor: selected === i ? theme.couponBg : "#FAFAFA",
2182
+ },
2183
+ ]}
2184
+ onPress={() => {
2185
+ setSelected(i);
2186
+ props.onInteraction?.({ mechanic: "nps_prompt", phase: "decision", value: i });
2187
+ }}
2188
+ accessibilityRole="radio"
2189
+ accessibilityLabel={String(i)}
2190
+ >
2191
+ <Text
2192
+ style={[
2193
+ nps.num,
2194
+ { color: selected === i ? theme.accent : theme.bodyColor },
2195
+ ]}
2196
+ >
2197
+ {i}
2198
+ </Text>
2199
+ </Pressable>
2200
+ ))}
2201
+ </ScrollView>
2202
+ {scaleLabels.length >= 2 ? (
2203
+ <View style={nps.labels}>
2204
+ <Text style={[nps.lbl, { color: theme.secondaryColor }]}>
2205
+ {scaleLabels[0]}
2206
+ </Text>
2207
+ <Text style={[nps.lbl, nps.lblEnd, { color: theme.secondaryColor }]}>
2208
+ {scaleLabels[scaleLabels.length - 1]}
2209
+ </Text>
2210
+ </View>
2211
+ ) : null}
2212
+ {selected !== null && feedbackEnabled ? (
2213
+ <TextInput
2214
+ value={feedback}
2215
+ onChangeText={setFeedback}
2216
+ multiline
2217
+ placeholder={typeof e.feedbackArea === "string" ? e.feedbackArea : "Tell us more (optional)"}
2218
+ placeholderTextColor={theme.secondaryColor}
2219
+ accessibilityLabel="Additional NPS feedback"
2220
+ style={[nps.feedback, { color: theme.titleColor, borderColor: theme.couponBorder }]}
2221
+ />
2222
+ ) : null}
2223
+ {selected !== null ? (
2224
+ props.spec.cta || props.spec.secondaryCta ? (
2225
+ <CtaArea {...props} beforePrimary={emitNps} />
2226
+ ) : (
2227
+ <PrimaryBtn
2228
+ label="SUBMIT"
2229
+ accent={theme.accent}
2230
+ onPress={() => {
2231
+ emitNps();
2232
+ if (!presentTerminalOutcome(props)) props.onDismiss();
2233
+ }}
2234
+ />
2235
+ )
2236
+ ) : null}
2237
+ </View>
2238
+ );
2239
+ }
2240
+
2241
+ const nps: Record<string, ViewStyle & TextStyle> = {
2242
+ row: { flexDirection: "row", gap: 6, paddingHorizontal: 2 } as any,
2243
+ cell: {
2244
+ minHeight: 44,
2245
+ minWidth: 44,
2246
+ paddingHorizontal: 8,
2247
+ paddingVertical: 8,
2248
+ borderRadius: 8,
2249
+ borderWidth: 1.5,
2250
+ justifyContent: "center",
2251
+ alignItems: "center",
2252
+ } as any,
2253
+ num: { fontSize: 13, fontWeight: "700" } as any,
2254
+ labels: {
2255
+ flexDirection: "row",
2256
+ justifyContent: "space-between",
2257
+ marginTop: 2,
2258
+ } as any,
2259
+ lbl: { flex: 1, fontSize: 10, fontWeight: "500" } as any,
2260
+ lblEnd: { textAlign: "right" } as any,
2261
+ feedback: { minHeight: 76, borderWidth: 1, borderRadius: 10, padding: 10, marginTop: 10, fontSize: 13 } as any,
2262
+ };
2263
+
2264
+ // ─── 7. Emoji Reaction Bar ────────────────────────────────────────────────────
2265
+
2266
+ export function EmojiReactionBarInApp(
2267
+ props: InAppBranchProps,
2268
+ ): React.ReactElement {
2269
+ const { spec, theme } = props;
2270
+ const e = ext(props);
2271
+ const reactions = arr(e.reactions).map(str);
2272
+ const emojis =
2273
+ reactions.length > 0 ? reactions : ["😍", "😊", "😐", "😕", "😡"];
2274
+ const [selected, setSelected] = useState<number | null>(null);
2275
+
2276
+ return (
2277
+ <View style={sh.content}>
2278
+ <Title text={spec.title} theme={theme} />
2279
+ <Body text={spec.body} theme={theme} />
2280
+ <View style={er.row}>
2281
+ {emojis.map((e, i) => (
2282
+ <Pressable
2283
+ key={i}
2284
+ style={[
2285
+ er.btn,
2286
+ selected === i && {
2287
+ backgroundColor: theme.couponBg,
2288
+ borderColor: theme.accent,
2289
+ },
2290
+ ]}
2291
+ onPress={() => {
2292
+ setSelected(i);
2293
+ props.onInteraction?.({ mechanic: "emoji_reaction_bar", phase: "decision", value: e });
2294
+ }}
2295
+ accessibilityRole="radio"
2296
+ accessibilityLabel={e}
2297
+ >
2298
+ <Text style={er.emoji}>{e}</Text>
2299
+ </Pressable>
2300
+ ))}
2301
+ </View>
2302
+ {selected !== null ? <CtaArea {...props} beforePrimary={() => props.onInteraction?.({ mechanic: "emoji_reaction_bar", phase: "resolved", value: emojis[selected] })} /> : null}
2303
+ </View>
2304
+ );
2305
+ }
2306
+
2307
+ const er: Record<string, ViewStyle & TextStyle> = {
2308
+ row: {
2309
+ flexDirection: "row",
2310
+ flexWrap: "wrap",
2311
+ justifyContent: "center",
2312
+ gap: 10,
2313
+ marginVertical: 14,
2314
+ } as any,
2315
+ btn: {
2316
+ alignItems: "center",
2317
+ justifyContent: "center",
2318
+ minHeight: 44,
2319
+ minWidth: 44,
2320
+ padding: 8,
2321
+ borderRadius: 12,
2322
+ borderWidth: 1.5,
2323
+ borderColor: "transparent",
2324
+ } as any,
2325
+ emoji: { fontSize: 32 } as any,
2326
+ };
2327
+
2328
+ // ─── 8. Star Rating ───────────────────────────────────────────────────────────
2329
+
2330
+ export function StarRatingInApp(props: InAppBranchProps): React.ReactElement {
2331
+ const { spec, theme } = props;
2332
+ const e = ext(props);
2333
+ const max = boundedInt(e.ratingScale, 5, 1, 10);
2334
+ const [rating, setRating] = useState(0);
2335
+ const threshold = boundedInt(e.storeReviewThreshold ?? e.ratingGate, 4, 1, Math.max(1, max));
2336
+ const complete = async (): Promise<void> => {
2337
+ props.onInteraction?.({ mechanic: "rating_request", phase: "resolved", value: rating, meta: { threshold } });
2338
+ if (rating >= threshold) {
2339
+ const ran = await requestNativeStoreReview();
2340
+ props.onInteraction?.({
2341
+ mechanic: "rating_request",
2342
+ phase: "completed",
2343
+ value: rating,
2344
+ meta: { route: ran ? "store_review" : "fallback", threshold },
2345
+ });
2346
+ if (ran) {
2347
+ if (presentTerminalOutcome(props)) return;
2348
+ props.onDismiss();
2349
+ return;
2350
+ }
2351
+ }
2352
+ if (presentTerminalOutcome(props)) return;
2353
+ if (!props.spec.cta || props.spec.cta.action === "dismiss") props.onDismiss();
2354
+ else props.onCta(props.spec.cta);
2355
+ };
2356
+
2357
+ return (
2358
+ <View style={sh.content}>
2359
+ <Title text={spec.title} theme={theme} />
2360
+ <Body text={spec.body} theme={theme} />
2361
+ <View style={sr.row}>
2362
+ {Array.from({ length: max }, (_, i) => (
2363
+ <Pressable
2364
+ key={i}
2365
+ style={sr.btn}
2366
+ onPress={() => {
2367
+ setRating(i + 1);
2368
+ props.onInteraction?.({ mechanic: "rating_request", phase: "decision", value: i + 1 });
2369
+ }}
2370
+ accessibilityRole="radio"
2371
+ accessibilityLabel={`${i + 1} star`}
2372
+ >
2373
+ <Text
2374
+ style={[sr.star, { color: i < rating ? "#F59E0B" : "#D1D5DB" }]}
2375
+ >
2376
+ ★
2377
+ </Text>
2378
+ </Pressable>
2379
+ ))}
2380
+ </View>
2381
+ {rating > 0 ? (
2382
+ <View style={sh.ctaRow}>
2383
+ {props.spec.secondaryCta ? <SecondaryBtn label={props.spec.secondaryCta.label} onPress={props.onDismiss} color={theme.secondaryColor} /> : null}
2384
+ <PrimaryBtn
2385
+ label={props.spec.cta?.label || (rating >= threshold ? "RATE APP" : "CONTINUE")}
2386
+ accent={theme.accent}
2387
+ onPress={() => { void complete(); }}
2388
+ />
2389
+ </View>
2390
+ ) : null}
2391
+ </View>
2392
+ );
2393
+ }
2394
+
2395
+ const sr: Record<string, ViewStyle & TextStyle> = {
2396
+ row: {
2397
+ flexDirection: "row",
2398
+ flexWrap: "wrap",
2399
+ justifyContent: "center",
2400
+ gap: 6,
2401
+ marginVertical: 14,
2402
+ } as any,
2403
+ btn: { alignItems: "center", justifyContent: "center", minHeight: 44, minWidth: 44 } as any,
2404
+ star: { fontSize: 38 } as any,
2405
+ };
2406
+
2407
+ // ─── 9. Streak Reward ─────────────────────────────────────────────────────────
2408
+
2409
+ export function StreakRewardInApp(props: InAppBranchProps): React.ReactElement {
2410
+ const { spec, theme } = props;
2411
+ const e = ext(props);
2412
+ const daily = variantOf(props) === "daily_check_in";
2413
+ const [gameOutcome, setGameOutcome] = useState<InAppGameOutcome | null>(null);
2414
+ const [gameLoading, setGameLoading] = useState(false);
2415
+ const [gameError, setGameError] = useState("");
2416
+ const loadStarted = useRef(false);
2417
+ const resolveState = async (): Promise<void> => {
2418
+ if (gameLoading) return;
2419
+ setGameLoading(true);
2420
+ setGameError("");
2421
+ props.onInteraction?.({
2422
+ mechanic: daily ? "daily_check_in" : "streak_reward",
2423
+ phase: "started",
2424
+ attempt: 1,
2425
+ });
2426
+ try {
2427
+ setGameOutcome(await requireGameOutcome(props));
2428
+ } catch (error) {
2429
+ setGameError(gameUnavailableText(error));
2430
+ } finally {
2431
+ setGameLoading(false);
2432
+ }
2433
+ };
2434
+ useEffect(() => {
2435
+ if (daily || loadStarted.current) return;
2436
+ loadStarted.current = true;
2437
+ void resolveState();
2438
+ }, [daily]);
2439
+
2440
+ const remoteState = gameOutcome?.state ?? {};
2441
+ const calendarDays = arr(remoteState.calendarDays);
2442
+ const authoredFilled = calendarDays.filter((day) => {
2443
+ const value = record(day);
2444
+ return day === true || value.done === true || value.checked === true || value.claimed === true;
2445
+ }).length;
2446
+ const total = boundedInt(remoteState.total ?? e.total, calendarDays.length || 7, 1, 31);
2447
+ const initialFilled = boundedInt(
2448
+ typeof remoteState.filled === "number"
2449
+ ? remoteState.filled
2450
+ : typeof remoteState.streakValue === "number"
2451
+ ? remoteState.streakValue
2452
+ : authoredFilled,
2453
+ 0,
2454
+ 0,
2455
+ total,
2456
+ );
2457
+ const authoredToday = calendarDays.find((day) => {
2458
+ const value = record(day);
2459
+ return value.today === true || value.current === true;
2460
+ });
2461
+ const todayState = record(authoredToday);
2462
+ const alreadyCheckedIn =
2463
+ initialFilled >= total ||
2464
+ authoredToday === true ||
2465
+ todayState.done === true ||
2466
+ todayState.checked === true ||
2467
+ todayState.claimed === true;
2468
+ const filled = initialFilled;
2469
+ const glyph = str(remoteState.glyph ?? e.glyph) || "🔥";
2470
+ const caption =
2471
+ gameResultLabel(gameOutcome) || str(remoteState.rewardMessage ?? e.rewardMessage) ||
2472
+ (filled > 0 ? `${filled}-day streak` : "");
2473
+
2474
+ const finishDaily = (): void => {
2475
+ if (presentTerminalOutcome(props, gameResultLabel(gameOutcome))) return;
2476
+ if (spec.cta && spec.cta.action !== "dismiss") props.onCta(spec.cta);
2477
+ else props.onDismiss();
2478
+ };
2479
+
2480
+ return (
2481
+ <View style={sh.content}>
2482
+ <Title text={spec.title} theme={theme} />
2483
+ <Body text={spec.body} theme={theme} />
2484
+ {!daily && (gameLoading || (!gameOutcome && gameError)) ? (
2485
+ <View style={[stk.claimed, { backgroundColor: theme.couponBg }]}>
2486
+ <Text style={[stk.claimedTitle, { color: theme.bodyColor }]}>
2487
+ {gameLoading ? "Loading your streak…" : gameError}
2488
+ </Text>
2489
+ {!gameLoading ? <PrimaryBtn label="RETRY" accent={theme.accent} onPress={() => { void resolveState(); }} /> : null}
2490
+ </View>
2491
+ ) : <View style={stk.row}>
2492
+ {Array.from({ length: total }, (_, i) => {
2493
+ const day = record(calendarDays[i]);
2494
+ const frozen = day.frozen === true || day.freeze === true;
2495
+ const completed = calendarDays.length > 0
2496
+ ? calendarDays[i] === true || day.done === true || day.checked === true || day.claimed === true
2497
+ : i < filled;
2498
+ return (
2499
+ <View
2500
+ key={i}
2501
+ style={[
2502
+ stk.dot,
2503
+ {
2504
+ backgroundColor: completed ? theme.couponBg : "#F1F3F7",
2505
+ borderColor: completed ? theme.accent : "#E4E7EC",
2506
+ },
2507
+ ]}
2508
+ >
2509
+ <Text style={stk.glyph}>{frozen ? "❄️" : completed ? glyph : ""}</Text>
2510
+ </View>
2511
+ );
2512
+ })}
2513
+ </View>}
2514
+ {caption ? (
2515
+ <Text style={[stk.caption, { color: theme.accent }]}>{caption}</Text>
2516
+ ) : null}
2517
+ {daily ? (
2518
+ gameOutcome ? (
2519
+ <View style={[stk.claimed, { backgroundColor: theme.couponBg }]} accessibilityLiveRegion="polite">
2520
+ <Text style={[stk.claimedTitle, { color: theme.accent }]}>
2521
+ {alreadyCheckedIn ? "✓ Checked in today" : gameResultLabel(gameOutcome) || "Check-in confirmed"}
2522
+ </Text>
2523
+ <PrimaryBtn label="DONE" accent={theme.accent} onPress={finishDaily} />
2524
+ </View>
2525
+ ) : (
2526
+ <>
2527
+ {gameError ? <Text style={[sl.unavailable, { color: theme.secondaryColor }]}>{gameError}</Text> : null}
2528
+ <PrimaryBtn
2529
+ label={gameLoading ? "CHECKING…" : gameError ? "RETRY" : spec.cta?.label || "CHECK IN"}
2530
+ accent={theme.accent}
2531
+ onPress={() => { void resolveState(); }}
2532
+ disabled={gameLoading}
2533
+ />
2534
+ </>
2535
+ )
2536
+ ) : gameOutcome ? <CtaArea {...props} /> : null}
2537
+ </View>
2538
+ );
2539
+ }
2540
+
2541
+ const stk: Record<string, ViewStyle & TextStyle> = {
2542
+ row: {
2543
+ flexDirection: "row",
2544
+ gap: 8,
2545
+ justifyContent: "center",
2546
+ marginVertical: 14,
2547
+ flexWrap: "wrap",
2548
+ } as any,
2549
+ dot: {
2550
+ width: 38,
2551
+ height: 38,
2552
+ borderRadius: 10,
2553
+ borderWidth: 1.5,
2554
+ justifyContent: "center",
2555
+ alignItems: "center",
2556
+ } as any,
2557
+ glyph: { fontSize: 18 } as any,
2558
+ caption: {
2559
+ textAlign: "center",
2560
+ fontWeight: "700",
2561
+ fontSize: 14,
2562
+ marginBottom: 12,
2563
+ } as any,
2564
+ claimed: { borderRadius: 12, padding: 12, gap: 8 } as any,
2565
+ claimedTitle: { fontSize: 14, fontWeight: "800", textAlign: "center" } as any,
2566
+ };
2567
+
2568
+ // ─── 10. Checklist Progress ────────────────────────────────────────────────────
2569
+
2570
+ export function ChecklistProgressInApp(
2571
+ props: InAppBranchProps,
2572
+ ): React.ReactElement {
2573
+ const { spec, theme } = props;
2574
+ const e = ext(props);
2575
+ const rawItems = arr(
2576
+ e.items ?? e.steps ?? e.tasks ?? e.checklistItems ?? e.setupSteps,
2577
+ );
2578
+ type Item = { label: string; done: boolean; locked: boolean };
2579
+ const initial: Item[] =
2580
+ rawItems.length > 0
2581
+ ? rawItems.map((item) => {
2582
+ const value = record(item);
2583
+ const completed = value.done === true || value.completed === true;
2584
+ return {
2585
+ label: labelOf(item),
2586
+ done: completed,
2587
+ locked: completed || value.interactive === false || value.readOnly === true,
2588
+ };
2589
+ })
2590
+ : [];
2591
+ const [items, setItems] = useState<Item[]>(initial);
2592
+ const done = items.filter((i) => i.done).length;
2593
+ const complete = items.length > 0 && done === items.length;
2594
+ const authoredComplete = initial.length > 0 && initial.every((item) => item.done);
2595
+
2596
+ return (
2597
+ <View style={sh.content}>
2598
+ <Title text={spec.title} theme={theme} />
2599
+ <Body text={spec.body} theme={theme} />
2600
+ {items.length > 0 ? (
2601
+ <View style={cl.progress}>
2602
+ <View style={[cl.bar, { backgroundColor: theme.couponBg }]}>
2603
+ <View
2604
+ style={[
2605
+ cl.fill,
2606
+ {
2607
+ width: `${(done / items.length) * 100}%` as any,
2608
+ backgroundColor: theme.accent,
2609
+ },
2610
+ ]}
2611
+ />
2612
+ </View>
2613
+ <Text style={[cl.pct, { color: theme.bodyColor }]}>
2614
+ {done}/{items.length}
2615
+ </Text>
2616
+ </View>
2617
+ ) : null}
2618
+ <View style={{ marginVertical: 8 }}>
2619
+ {items.map((item, i) => (
2620
+ <Pressable
2621
+ key={i}
2622
+ style={[cl.row, { borderBottomColor: "#F1F3F7" }]}
2623
+ onPress={() =>
2624
+ setItems((prev) =>
2625
+ prev.map((it, idx) =>
2626
+ idx === i && !it.locked ? { ...it, done: !it.done } : it,
2627
+ ),
2628
+ )
2629
+ }
2630
+ onPressOut={() => {
2631
+ if (item.locked) return;
2632
+ const nextDone = done + (item.done ? -1 : 1);
2633
+ props.onInteraction?.({
2634
+ mechanic: "interactive_checklist",
2635
+ phase: "progress",
2636
+ progress: nextDone / items.length,
2637
+ value: nextDone,
2638
+ });
2639
+ }}
2640
+ disabled={item.locked}
2641
+ accessibilityRole="checkbox"
2642
+ accessibilityState={{ checked: item.done, disabled: item.locked }}
2643
+ >
2644
+ <View
2645
+ style={[
2646
+ cl.check,
2647
+ {
2648
+ borderColor: item.done ? theme.accent : "#D1D5DB",
2649
+ backgroundColor: item.done ? theme.accent : "transparent",
2650
+ },
2651
+ ]}
2652
+ >
2653
+ {item.done ? <Text style={cl.tick}>✓</Text> : null}
2654
+ </View>
2655
+ <Text
2656
+ style={[
2657
+ cl.label,
2658
+ {
2659
+ color: item.done ? theme.bodyColor : theme.titleColor,
2660
+ textDecorationLine: item.done ? "line-through" : "none",
2661
+ },
2662
+ ]}
2663
+ >
2664
+ {item.label}
2665
+ </Text>
2666
+ </Pressable>
2667
+ ))}
2668
+ </View>
2669
+ {complete ? (
2670
+ <View style={[cl.complete, { backgroundColor: theme.couponBg }]} accessibilityLiveRegion="polite">
2671
+ <Text style={[cl.completeTitle, { color: theme.accent }]}>
2672
+ {authoredComplete ? str(e.completionLabel) || "Checklist complete" : "Ready to finish"}
2673
+ </Text>
2674
+ {authoredComplete && labelOf(e.reward) ? <Text style={[cl.completeReward, { color: theme.bodyColor }]}>{labelOf(e.reward)}</Text> : null}
2675
+ <CtaArea {...props} />
2676
+ </View>
2677
+ ) : null}
2678
+ </View>
2679
+ );
2680
+ }
2681
+
2682
+ const cl: Record<string, ViewStyle & TextStyle> = {
2683
+ progress: {
2684
+ flexDirection: "row",
2685
+ alignItems: "center",
2686
+ gap: 8,
2687
+ marginBottom: 10,
2688
+ } as any,
2689
+ bar: { flex: 1, height: 8, borderRadius: 999, overflow: "hidden" } as any,
2690
+ fill: { height: "100%", borderRadius: 999 } as any,
2691
+ pct: {
2692
+ fontSize: 11,
2693
+ fontWeight: "600",
2694
+ minWidth: 32,
2695
+ textAlign: "right",
2696
+ } as any,
2697
+ row: {
2698
+ flexDirection: "row",
2699
+ alignItems: "center",
2700
+ gap: 10,
2701
+ paddingVertical: 10,
2702
+ borderBottomWidth: 1,
2703
+ } as any,
2704
+ check: {
2705
+ width: 22,
2706
+ height: 22,
2707
+ borderRadius: 6,
2708
+ borderWidth: 1.8,
2709
+ justifyContent: "center",
2710
+ alignItems: "center",
2711
+ flexShrink: 0,
2712
+ } as any,
2713
+ tick: { color: "#fff", fontSize: 12, fontWeight: "800" } as any,
2714
+ label: { flex: 1, fontSize: 13, fontWeight: "500" } as any,
2715
+ complete: { borderRadius: 12, padding: 12, marginTop: 8 } as any,
2716
+ completeTitle: { fontSize: 14, fontWeight: "800", textAlign: "center" } as any,
2717
+ completeReward: { fontSize: 12, textAlign: "center", marginTop: 3 } as any,
2718
+ };
2719
+
2720
+ function normalizedProgress(value: unknown): number | null {
2721
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
2722
+ return Math.max(0, Math.min(100, value <= 1 ? value * 100 : value));
2723
+ }
2724
+
2725
+ function ProgressSummaryInApp(props: InAppBranchProps): React.ReactElement {
2726
+ const { spec, theme } = props;
2727
+ const e = ext(props);
2728
+ const backendOwned = variantOf(props) === "progress_unlock";
2729
+ const [gameOutcome, setGameOutcome] = useState<InAppGameOutcome | null>(null);
2730
+ const [gameLoading, setGameLoading] = useState(backendOwned);
2731
+ const [gameError, setGameError] = useState("");
2732
+ const loadStarted = useRef(false);
2733
+ const loadProgress = async (): Promise<void> => {
2734
+ setGameLoading(true);
2735
+ setGameError("");
2736
+ try {
2737
+ const outcome = await requireGameOutcome(props);
2738
+ setGameOutcome(outcome);
2739
+ props.onInteraction?.({ mechanic: "progress_unlock", phase: "resolved", result: gameResultLabel(outcome) || outcome.id });
2740
+ } catch (error) {
2741
+ setGameError(gameUnavailableText(error));
2742
+ } finally {
2743
+ setGameLoading(false);
2744
+ }
2745
+ };
2746
+ useEffect(() => {
2747
+ if (!backendOwned || loadStarted.current) return;
2748
+ loadStarted.current = true;
2749
+ void loadProgress();
2750
+ }, [backendOwned]);
2751
+ if (backendOwned && (gameLoading || !gameOutcome)) {
2752
+ return (
2753
+ <View style={sh.content}>
2754
+ <Title text={spec.title} theme={theme} />
2755
+ <Body text={spec.body} theme={theme} />
2756
+ <View style={[prog.rewardCard, { backgroundColor: theme.couponBg }]}>
2757
+ <Text style={[prog.reward, { color: theme.bodyColor }]}>
2758
+ {gameLoading ? "Loading your progress…" : gameError || "Progress unavailable."}
2759
+ </Text>
2760
+ {!gameLoading ? <PrimaryBtn label="RETRY" accent={theme.accent} onPress={() => { void loadProgress(); }} /> : null}
2761
+ </View>
2762
+ </View>
2763
+ );
2764
+ }
2765
+ const state = backendOwned ? (gameOutcome?.state ?? {}) : e;
2766
+ const steps = arr(state.steps ?? (backendOwned ? e.milestoneLabels : e.setupSteps ?? e.milestoneLabels));
2767
+ const doneSteps = steps.filter((step) => {
2768
+ const value = record(step);
2769
+ return value.done === true || value.completed === true;
2770
+ }).length;
2771
+ const firstNumber = (values: unknown[]): number | null => {
2772
+ const value = values.find(
2773
+ (candidate) =>
2774
+ typeof candidate === "number" && Number.isFinite(candidate),
2775
+ );
2776
+ return typeof value === "number" ? value : null;
2777
+ };
2778
+ const direct = firstNumber([state.progress, state.progressValue]);
2779
+ const current = firstNumber([
2780
+ state.currentValue,
2781
+ state.completedSteps,
2782
+ state.progressSource,
2783
+ state.goalSource,
2784
+ ]);
2785
+ const target = firstNumber([state.targetValue, state.goalValue]);
2786
+ const stepPosition = firstNumber([state.currentStep, state.stepIndex]);
2787
+ let progress = normalizedProgress(direct);
2788
+ if (progress === null && current !== null && target !== null && target > 0) {
2789
+ progress = normalizedProgress(current / target);
2790
+ }
2791
+ if (progress === null && steps.length > 0) {
2792
+ const completed = stepPosition ?? doneSteps;
2793
+ progress = normalizedProgress(completed / steps.length);
2794
+ }
2795
+ progress ??= 0;
2796
+ const unlocked = progress >= 100;
2797
+
2798
+ const segmentCount = steps.length >= 2 ? Math.min(10, steps.length) : 8;
2799
+ const filledSegments = Math.round((progress / 100) * segmentCount);
2800
+ const message =
2801
+ firstText(e, [
2802
+ "message",
2803
+ "motivationalText",
2804
+ "onboardingMessage",
2805
+ "rewardMessage",
2806
+ "goal",
2807
+ ]) || spec.body;
2808
+ const reward = backendOwned ? gameResultLabel(gameOutcome) : labelOf(e.reward);
2809
+ const nextStep = steps.find((step) => {
2810
+ const value = record(step);
2811
+ return value.done !== true && value.completed !== true;
2812
+ });
2813
+ const labels = arr(e.milestoneLabels).map(labelOf).filter(Boolean);
2814
+
2815
+ return (
2816
+ <View style={sh.content}>
2817
+ <Title
2818
+ text={
2819
+ spec.title ||
2820
+ firstText(e, ["challengeTitle", "celebrationTitle", "goalTitle"])
2821
+ }
2822
+ theme={theme}
2823
+ />
2824
+ <Body text={message} theme={theme} />
2825
+ <View style={prog.header}>
2826
+ <Text style={[prog.metric, { color: theme.accent }]}>
2827
+ {current !== null && target !== null
2828
+ ? `${current}/${target}`
2829
+ : `${Math.round(progress)}%`}
2830
+ </Text>
2831
+ {nextStep ? (
2832
+ <Text
2833
+ numberOfLines={1}
2834
+ style={[prog.next, { color: theme.bodyColor }]}
2835
+ >
2836
+ {labelOf(nextStep)}
2837
+ </Text>
2838
+ ) : null}
2839
+ </View>
2840
+ <View
2841
+ style={prog.segments}
2842
+ accessibilityRole="progressbar"
2843
+ accessibilityValue={{ min: 0, max: 100, now: Math.round(progress) }}
2844
+ >
2845
+ {Array.from({ length: segmentCount }, (_, index) => (
2846
+ <View
2847
+ key={index}
2848
+ style={[
2849
+ prog.segment,
2850
+ {
2851
+ backgroundColor:
2852
+ index < filledSegments ? theme.accent : theme.couponBg,
2853
+ },
2854
+ ]}
2855
+ />
2856
+ ))}
2857
+ </View>
2858
+ {labels.length > 1 ? (
2859
+ <View style={prog.labels}>
2860
+ <Text style={[prog.label, { color: theme.secondaryColor }]}>
2861
+ {labels[0]}
2862
+ </Text>
2863
+ <Text style={[prog.label, { color: theme.secondaryColor }]}>
2864
+ {labels[labels.length - 1]}
2865
+ </Text>
2866
+ </View>
2867
+ ) : null}
2868
+ {reward ? (
2869
+ <View style={[prog.rewardCard, { backgroundColor: theme.couponBg }]}>
2870
+ <Text style={[prog.reward, { color: unlocked ? theme.accent : theme.bodyColor }]}>
2871
+ {unlocked ? "✓ Unlocked" : "Unlocks"}: {reward}
2872
+ </Text>
2873
+ </View>
2874
+ ) : null}
2875
+ <CtaArea {...props} />
2876
+ </View>
2877
+ );
2878
+ }
2879
+
2880
+ const prog: Record<string, ViewStyle & TextStyle> = {
2881
+ header: {
2882
+ flexDirection: "row",
2883
+ alignItems: "center",
2884
+ gap: 10,
2885
+ marginTop: 12,
2886
+ } as any,
2887
+ metric: { fontSize: 20, fontWeight: "800" } as any,
2888
+ next: { flex: 1, fontSize: 12, textAlign: "right" } as any,
2889
+ segments: { flexDirection: "row", gap: 4, marginTop: 8 } as any,
2890
+ segment: { flex: 1, height: 9, borderRadius: 5 } as any,
2891
+ labels: {
2892
+ flexDirection: "row",
2893
+ justifyContent: "space-between",
2894
+ marginTop: 5,
2895
+ } as any,
2896
+ label: { fontSize: 10 } as any,
2897
+ reward: { fontSize: 12, fontWeight: "600", marginTop: 10 } as any,
2898
+ rewardCard: { borderRadius: 10, padding: 10, marginTop: 10 } as any,
2899
+ };
2900
+
2901
+ function CommunityChallengeInApp(props: InAppBranchProps): React.ReactElement {
2902
+ const { spec, theme } = props;
2903
+ const e = ext(props);
2904
+ const [joined, setJoined] = useState(false);
2905
+ const participants = boundedInt(e.participantsCount, 0, 0, 999_999_999);
2906
+ const leaders = arr(e.leaderboard).slice(0, 5);
2907
+ const title = str(e.challengeTitle) || spec.title;
2908
+ const goal = str(e.goal) || spec.body;
2909
+ const reward = labelOf(e.reward);
2910
+
2911
+ return (
2912
+ <View style={sh.content}>
2913
+ <Title text={title} theme={theme} />
2914
+ <Body text={goal} theme={theme} />
2915
+ <View style={[challenge.hero, { backgroundColor: theme.couponBg }]}>
2916
+ <Text style={challenge.emoji}>{joined ? "🏁" : "🏆"}</Text>
2917
+ <Text style={[challenge.count, { color: theme.accent }]}>{participants.toLocaleString()}</Text>
2918
+ <Text style={[challenge.caption, { color: theme.bodyColor }]}>participants</Text>
2919
+ </View>
2920
+ {leaders.length > 0 ? (
2921
+ <View style={challenge.board}>
2922
+ {leaders.map((leader, index) => (
2923
+ <View key={index} style={challenge.row}>
2924
+ <Text style={[challenge.rank, { color: theme.accent }]}>#{index + 1}</Text>
2925
+ <Text style={[challenge.name, { color: theme.titleColor }]}>{labelOf(leader)}</Text>
2926
+ <Text style={[challenge.value, { color: theme.bodyColor }]}>
2927
+ {typeof (record(leader).value ?? record(leader).score) === "number"
2928
+ ? String(record(leader).value ?? record(leader).score)
2929
+ : str(record(leader).value ?? record(leader).score)}
2930
+ </Text>
2931
+ </View>
2932
+ ))}
2933
+ </View>
2934
+ ) : null}
2935
+ {reward ? <Text style={[challenge.reward, { color: theme.bodyColor }]}>Reward: {reward}</Text> : null}
2936
+ {joined ? (
2937
+ <View style={[challenge.joined, { borderColor: theme.accent }]} accessibilityLiveRegion="polite">
2938
+ <Text style={[challenge.joinedText, { color: theme.accent }]}>Join request sent</Text>
2939
+ <PrimaryBtn
2940
+ label="DONE"
2941
+ accent={theme.accent}
2942
+ onPress={() => {
2943
+ if (!presentTerminalOutcome(props, "Join request sent")) props.onDismiss();
2944
+ }}
2945
+ />
2946
+ </View>
2947
+ ) : (
2948
+ <PrimaryBtn
2949
+ label={str(e.joinLabel) || spec.cta?.label || "JOIN CHALLENGE"}
2950
+ accent={theme.accent}
2951
+ onPress={() => {
2952
+ setJoined(true);
2953
+ props.onInteraction?.({ mechanic: "community_challenge", phase: "started" });
2954
+ if (spec.cta && spec.cta.action !== "dismiss") props.onCta(spec.cta);
2955
+ }}
2956
+ />
2957
+ )}
2958
+ </View>
2959
+ );
2960
+ }
2961
+
2962
+ const challenge: Record<string, ViewStyle & TextStyle> = {
2963
+ hero: { borderRadius: 16, padding: 16, alignItems: "center", marginVertical: 12 } as any,
2964
+ emoji: { fontSize: 36 } as any,
2965
+ count: { fontSize: 24, fontWeight: "900", marginTop: 3 } as any,
2966
+ caption: { fontSize: 11, fontWeight: "600" } as any,
2967
+ board: { marginBottom: 10 } as any,
2968
+ row: { flexDirection: "row", alignItems: "center", paddingVertical: 7, gap: 8 } as any,
2969
+ rank: { width: 28, fontSize: 12, fontWeight: "800" } as any,
2970
+ name: { flex: 1, fontSize: 13, fontWeight: "600" } as any,
2971
+ value: { fontSize: 12, fontWeight: "700" } as any,
2972
+ reward: { fontSize: 12, fontWeight: "700", textAlign: "center", marginBottom: 10 } as any,
2973
+ joined: { borderWidth: 1.5, borderRadius: 12, padding: 12, gap: 8 } as any,
2974
+ joinedText: { fontSize: 13, fontWeight: "800", textAlign: "center" } as any,
2975
+ };
2976
+
2977
+ // ─── 11. Coupon Wallet ─────────────────────────────────────────────────────────
2978
+
2979
+ export function CouponWalletInApp(props: InAppBranchProps): React.ReactElement {
2980
+ const { spec, theme } = props;
2981
+ const e = ext(props);
2982
+ const coupons = arr(e.coupons)
2983
+ .map((coupon) => {
2984
+ const value = record(coupon);
2985
+ return {
2986
+ code: str(value.code ?? value.couponCode),
2987
+ label: str(value.label ?? value.title),
2988
+ };
2989
+ })
2990
+ .filter((coupon) => coupon.code);
2991
+ const fallbackCode =
2992
+ str(e.couponCode ?? e.referralCode ?? e.code) || str(spec.coupon);
2993
+ const fallbackLabel =
2994
+ str(e.couponLabel ?? e.referralLabel) || "Your exclusive coupon";
2995
+ const entries =
2996
+ coupons.length > 0
2997
+ ? coupons
2998
+ : fallbackCode
2999
+ ? [{ code: fallbackCode, label: fallbackLabel }]
3000
+ : [];
3001
+
3002
+ return (
3003
+ <View style={sh.content}>
3004
+ <Title text={spec.title} theme={theme} />
3005
+ <Body text={spec.body} theme={theme} />
3006
+ {entries.map(({ code, label }, index) => (
3007
+ <View
3008
+ key={`${code}-${index}`}
3009
+ style={[
3010
+ cw.chip,
3011
+ {
3012
+ backgroundColor: theme.couponBg,
3013
+ borderColor: theme.couponBorder,
3014
+ },
3015
+ ]}
3016
+ accessibilityLabel={`Coupon code ${code}`}
3017
+ >
3018
+ <Text style={[cw.label, { color: theme.bodyColor }]}>
3019
+ {label || fallbackLabel}
3020
+ </Text>
3021
+ <Text selectable style={[cw.code, { color: theme.couponFg }]}>
3022
+ {code}
3023
+ </Text>
3024
+ <Text style={[cw.tapHint, { color: theme.accent }]}>
3025
+ Press and hold to select
3026
+ </Text>
3027
+ </View>
3028
+ ))}
3029
+ <CtaArea {...props} />
3030
+ </View>
3031
+ );
3032
+ }
3033
+
3034
+ const cw: Record<string, ViewStyle & TextStyle> = {
3035
+ chip: {
3036
+ borderRadius: 14,
3037
+ borderWidth: 1.5,
3038
+ borderStyle: "dashed",
3039
+ padding: 18,
3040
+ alignItems: "center",
3041
+ marginVertical: 14,
3042
+ } as any,
3043
+ label: {
3044
+ fontSize: 11,
3045
+ fontWeight: "600",
3046
+ marginBottom: 6,
3047
+ textTransform: "uppercase",
3048
+ letterSpacing: 0.5,
3049
+ } as any,
3050
+ code: {
3051
+ fontSize: 26,
3052
+ fontWeight: "900",
3053
+ letterSpacing: 3,
3054
+ marginBottom: 6,
3055
+ } as any,
3056
+ tapHint: { fontSize: 12, fontWeight: "600" } as any,
3057
+ };
3058
+
3059
+ // ─── QR / NFC / Wallet pass ──────────────────────────────────────────────────
3060
+
3061
+ export function QrNfcInApp(props: InAppBranchProps): React.ReactElement {
3062
+ const { spec, theme } = props;
3063
+ const e = ext(props);
3064
+ const qr = str(spec.assets?.qr);
3065
+ const code = firstText(e, ["codeValue", "barcode", "fallbackLink"]);
3066
+ const kind = firstText(e, ["codeType", "passType", "walletProvider"]);
3067
+ const instruction = firstText(e, ["instruction", "passTitle"]);
3068
+ const expiry = str(e.expiry);
3069
+ const wallet = variantOf(props) === "wallet_pass_offer";
3070
+ const walletAsset = str(props.spec.assets?.wallet);
3071
+
3072
+ return (
3073
+ <View style={sh.content}>
3074
+ <Title text={spec.title || str(e.passTitle)} theme={theme} />
3075
+ <Body text={instruction || spec.body} theme={theme} />
3076
+ {wallet ? (
3077
+ <View style={[qrn.pass, { backgroundColor: theme.couponBg, borderColor: theme.couponBorder }]}>
3078
+ <Text style={qrn.passIcon}>▤</Text>
3079
+ <View style={{ flex: 1 }}>
3080
+ <Text style={[qrn.passTitle, { color: theme.titleColor }]}>{str(e.passTitle) || spec.title || "Wallet pass"}</Text>
3081
+ <Text style={[qrn.kind, { color: theme.bodyColor }]}>{kind || "Mobile wallet"}</Text>
3082
+ </View>
3083
+ {walletAsset ? <Text style={[qrn.ready, { color: theme.accent }]}>Ready</Text> : null}
3084
+ </View>
3085
+ ) : null}
3086
+ {qr ? (
3087
+ <Image
3088
+ source={{ uri: qr }}
3089
+ resizeMode="contain"
3090
+ style={qrn.image as any}
3091
+ accessibilityLabel="Campaign QR code"
3092
+ />
3093
+ ) : code ? (
3094
+ <View
3095
+ style={[
3096
+ qrn.code,
3097
+ {
3098
+ backgroundColor: theme.couponBg,
3099
+ borderColor: theme.couponBorder,
3100
+ },
3101
+ ]}
3102
+ >
3103
+ {kind ? (
3104
+ <Text style={[qrn.kind, { color: theme.bodyColor }]}>{kind}</Text>
3105
+ ) : null}
3106
+ <Text selectable style={[qrn.value, { color: theme.couponFg }]}>
3107
+ {code}
3108
+ </Text>
3109
+ </View>
3110
+ ) : wallet && walletAsset ? null : (
3111
+ <View style={[qrn.notice, { backgroundColor: theme.couponBg }]}>
3112
+ <Text style={[qrn.noticeText, { color: theme.bodyColor }]}>
3113
+ No scannable code was provided. Use the campaign action to continue.
3114
+ </Text>
3115
+ </View>
3116
+ )}
3117
+ {expiry ? (
3118
+ <Text style={[qrn.expiry, { color: theme.secondaryColor }]}>
3119
+ Expires {expiry}
3120
+ </Text>
3121
+ ) : null}
3122
+ <CtaArea {...props} beforePrimary={() => wallet ? props.onInteraction?.({
3123
+ mechanic: "wallet_pass_offer",
3124
+ phase: "started",
3125
+ value: walletAsset || code || "host_action",
3126
+ meta: { hostAction: "add_to_wallet", wallet: walletAsset || undefined },
3127
+ }) : props.onInteraction?.({
3128
+ mechanic: "nfc_qr_prompt",
3129
+ phase: "started",
3130
+ value: str(props.spec.assets?.nfc) || code || "host_action",
3131
+ })} />
3132
+ </View>
3133
+ );
3134
+ }
3135
+
3136
+ const qrn: Record<string, ViewStyle & TextStyle> = {
3137
+ image: {
3138
+ alignSelf: "center",
3139
+ aspectRatio: 1,
3140
+ marginTop: 12,
3141
+ maxWidth: 184,
3142
+ width: "100%",
3143
+ } as any,
3144
+ code: {
3145
+ borderWidth: 1,
3146
+ borderRadius: 12,
3147
+ padding: 14,
3148
+ marginTop: 12,
3149
+ alignItems: "center",
3150
+ } as any,
3151
+ kind: { fontSize: 10, fontWeight: "700", textTransform: "uppercase" } as any,
3152
+ value: {
3153
+ fontSize: 15,
3154
+ lineHeight: 21,
3155
+ fontWeight: "700",
3156
+ textAlign: "center",
3157
+ marginTop: 4,
3158
+ } as any,
3159
+ notice: { borderRadius: 12, padding: 12, marginTop: 12 } as any,
3160
+ noticeText: { fontSize: 12, lineHeight: 18, textAlign: "center" } as any,
3161
+ expiry: { fontSize: 11, textAlign: "center", marginTop: 8 } as any,
3162
+ pass: { flexDirection: "row", alignItems: "center", gap: 10, borderWidth: 1, borderRadius: 14, padding: 14, marginTop: 12 } as any,
3163
+ passIcon: { fontSize: 30 } as any,
3164
+ passTitle: { fontSize: 14, fontWeight: "800" } as any,
3165
+ ready: { fontSize: 11, fontWeight: "800" } as any,
3166
+ };
3167
+
3168
+ // ─── Video commerce ──────────────────────────────────────────────────────────
3169
+
3170
+ export function VideoCommerceInApp(
3171
+ props: InAppBranchProps,
3172
+ ): React.ReactElement {
3173
+ const { spec, theme, onCta, onDismiss } = props;
3174
+ const e = ext(props);
3175
+ const products = arr(e.products ?? spec.assets?.products);
3176
+ const firstProduct = record(products[0]);
3177
+ const firstProductImage = arr(e.productImages)[0] ?? firstProduct.image ?? firstProduct.imageUrl;
3178
+ const poster =
3179
+ str(spec.assets?.image) ||
3180
+ str(spec.imageUrl) ||
3181
+ mediaUri(e.posterImage ?? firstProductImage);
3182
+ const video = str(spec.assets?.video) || str(e.video);
3183
+ const mediaHeight = resolveInAppMediaHeight(spec, 180, 360);
3184
+ const product = firstText(e, ["productTitle", "itemName"]) || labelOf(firstProduct);
3185
+ const price = str(e.price) || str(firstProduct.price);
3186
+ const discount = str(e.discount) || str(firstProduct.discount);
3187
+
3188
+ const act = () => {
3189
+ if (!spec.cta) return;
3190
+ spec.cta.action === "dismiss" ? onDismiss() : onCta(spec.cta);
3191
+ };
3192
+
3193
+ return (
3194
+ <View style={sh.content}>
3195
+ <Title text={spec.title} theme={theme} />
3196
+ <Body text={spec.body} theme={theme} />
3197
+ <View style={[vid.poster, { backgroundColor: theme.couponBg }]}>
3198
+ {poster ? (
3199
+ <Image
3200
+ source={{ uri: poster }}
3201
+ resizeMode="cover"
3202
+ style={[
3203
+ vid.image,
3204
+ mediaHeight === undefined ? null : { height: mediaHeight },
3205
+ ] as any}
3206
+ accessibilityLabel={product || spec.title || "Product video"}
3207
+ />
3208
+ ) : null}
3209
+ {video ? (
3210
+ <Pressable
3211
+ style={[vid.play, { backgroundColor: theme.cardBg }]}
3212
+ disabled={!spec.cta}
3213
+ onPress={act}
3214
+ accessibilityRole="button"
3215
+ accessibilityLabel="Play video through campaign action"
3216
+ >
3217
+ <Text style={[vid.playIcon, { color: theme.accent }]}>▶</Text>
3218
+ <Text style={[vid.playLabel, { color: theme.titleColor }]}>
3219
+ {str(e.playLabel) || "Play video"}
3220
+ </Text>
3221
+ </Pressable>
3222
+ ) : null}
3223
+ </View>
3224
+ {product || price || discount ? (
3225
+ <View style={vid.product}>
3226
+ <View style={vid.productCopy}>
3227
+ {product ? (
3228
+ <Text style={[vid.productTitle, { color: theme.titleColor }]}>
3229
+ {product}
3230
+ </Text>
3231
+ ) : null}
3232
+ {discount ? (
3233
+ <Text style={[vid.discount, { color: theme.accent }]}>
3234
+ {discount}
3235
+ </Text>
3236
+ ) : null}
3237
+ </View>
3238
+ {price ? (
3239
+ <Text style={[vid.price, { color: theme.titleColor }]}>
3240
+ {price}
3241
+ </Text>
3242
+ ) : null}
3243
+ </View>
3244
+ ) : null}
3245
+ <CtaArea {...props} />
3246
+ </View>
3247
+ );
3248
+ }
3249
+
3250
+ const vid: Record<string, ViewStyle & TextStyle> = {
3251
+ poster: {
3252
+ minHeight: 150,
3253
+ borderRadius: 14,
3254
+ marginTop: 10,
3255
+ overflow: "hidden",
3256
+ alignItems: "center",
3257
+ justifyContent: "center",
3258
+ } as any,
3259
+ image: { width: "100%", height: 180 } as any,
3260
+ play: {
3261
+ position: "absolute",
3262
+ flexDirection: "row",
3263
+ alignItems: "center",
3264
+ gap: 7,
3265
+ borderRadius: 999,
3266
+ maxWidth: "90%",
3267
+ paddingVertical: 9,
3268
+ paddingHorizontal: 13,
3269
+ } as any,
3270
+ playIcon: { fontSize: 15 } as any,
3271
+ playLabel: { flexShrink: 1, fontSize: 12, fontWeight: "700", textAlign: "center" } as any,
3272
+ product: {
3273
+ flexDirection: "row",
3274
+ alignItems: "center",
3275
+ marginTop: 10,
3276
+ gap: 10,
3277
+ } as any,
3278
+ productCopy: { flex: 1 } as any,
3279
+ productTitle: { fontSize: 14, fontWeight: "700" } as any,
3280
+ discount: { fontSize: 11, fontWeight: "700", marginTop: 2 } as any,
3281
+ price: { fontSize: 15, fontWeight: "800" } as any,
3282
+ };
3283
+
3284
+ // ─── Celebration ─────────────────────────────────────────────────────────────
3285
+
3286
+ const BURST_PARTICLES = ["●", "✦", "●", "◆", "●", "✦", "●", "◆"];
3287
+
3288
+ export function CelebrationInApp(props: InAppBranchProps): React.ReactElement {
3289
+ const { spec, theme } = props;
3290
+ const e = ext(props);
3291
+ const burst = useRef(new Animated.Value(0)).current;
3292
+ const reducedMotion = useReducedMotion();
3293
+ const title =
3294
+ spec.title || firstText(e, ["celebrationTitle", "milestoneName"]);
3295
+ const message = firstText(e, ["celebrationMessage", "message"]) || spec.body;
3296
+ // Reward copy is display-only; the renderer never decides or grants a reward.
3297
+ const reward = labelOf(e.reward);
3298
+ const confettiEnabled = e.confetti !== false && e.confettiAnimation !== false;
3299
+
3300
+ useEffect(() => {
3301
+ if (reducedMotion !== null) {
3302
+ burst.stopAnimation();
3303
+ burst.setValue(0);
3304
+ if (!reducedMotion && confettiEnabled) {
3305
+ Animated.timing(burst, {
3306
+ toValue: 1,
3307
+ duration: 750,
3308
+ easing: Easing.out(Easing.cubic),
3309
+ useNativeDriver: true,
3310
+ }).start();
3311
+ }
3312
+ }
3313
+ return () => burst.stopAnimation();
3314
+ }, [burst, confettiEnabled, reducedMotion]);
3315
+
3316
+ return (
3317
+ <View style={sh.content}>
3318
+ <View style={cel.stage}>
3319
+ {confettiEnabled ? BURST_PARTICLES.map((particle, index) => {
3320
+ const angle = (index / BURST_PARTICLES.length) * Math.PI * 2;
3321
+ const distance = index % 2 === 0 ? 54 : 70;
3322
+ const translateX = burst.interpolate({
3323
+ inputRange: [0, 1],
3324
+ outputRange: [0, Math.cos(angle) * distance],
3325
+ });
3326
+ const translateY = burst.interpolate({
3327
+ inputRange: [0, 1],
3328
+ outputRange: [0, Math.sin(angle) * distance],
3329
+ });
3330
+ const opacity = burst.interpolate({
3331
+ inputRange: [0, 0.25, 1],
3332
+ outputRange: [0, 1, 0.35],
3333
+ });
3334
+ return (
3335
+ <Animated.Text
3336
+ key={index}
3337
+ style={[
3338
+ cel.particle,
3339
+ {
3340
+ color: index % 2 === 0 ? theme.accent : "#F59E0B",
3341
+ opacity,
3342
+ transform: [{ translateX }, { translateY }],
3343
+ },
3344
+ ]}
3345
+ >
3346
+ {particle}
3347
+ </Animated.Text>
3348
+ );
3349
+ }) : null}
3350
+ <Text style={cel.emoji}>{str(e.emoji) || "🎉"}</Text>
3351
+ </View>
3352
+ <Title text={title} theme={theme} />
3353
+ <Body text={message} theme={theme} />
3354
+ {reward ? (
3355
+ <View style={[cel.reward, { backgroundColor: theme.couponBg }]}>
3356
+ <Text style={[cel.rewardText, { color: theme.couponFg }]}>
3357
+ Reward: {reward}
3358
+ </Text>
3359
+ </View>
3360
+ ) : null}
3361
+ <CtaArea {...props} />
3362
+ </View>
3363
+ );
3364
+ }
3365
+
3366
+ const cel: Record<string, ViewStyle & TextStyle> = {
3367
+ stage: {
3368
+ height: 150,
3369
+ overflow: "hidden",
3370
+ alignItems: "center",
3371
+ justifyContent: "center",
3372
+ } as any,
3373
+ particle: { position: "absolute", fontSize: 15, fontWeight: "900" } as any,
3374
+ emoji: { fontSize: 52 } as any,
3375
+ reward: { borderRadius: 10, padding: 10, marginTop: 10 } as any,
3376
+ rewardText: { fontSize: 13, fontWeight: "700", textAlign: "center" } as any,
3377
+ };
3378
+
3379
+ function FeedbackCaptureInApp(props: InAppBranchProps): React.ReactElement {
3380
+ const { spec, theme } = props;
3381
+ const e = ext(props);
3382
+ const [feedback, setFeedback] = useState("");
3383
+ const prompt = str(e.promptText) || spec.body;
3384
+
3385
+ return (
3386
+ <View style={sh.content}>
3387
+ <Title text={spec.title} theme={theme} />
3388
+ <Body text={prompt} theme={theme} />
3389
+ <TextInput
3390
+ value={feedback}
3391
+ onChangeText={setFeedback}
3392
+ multiline
3393
+ textAlignVertical="top"
3394
+ placeholder={str(e.placeholder) || "Type your feedback"}
3395
+ placeholderTextColor={theme.secondaryColor}
3396
+ accessibilityLabel={prompt || "Feedback"}
3397
+ style={[
3398
+ fb.input,
3399
+ {
3400
+ color: theme.titleColor,
3401
+ borderColor: theme.couponBorder,
3402
+ backgroundColor: theme.cardBg,
3403
+ },
3404
+ ]}
3405
+ />
3406
+ {feedback.trim() ? <CtaArea {...props} beforePrimary={() => props.onInteraction?.({
3407
+ mechanic: "feedback_capture",
3408
+ phase: "resolved",
3409
+ result: feedback.trim(),
3410
+ meta: { feedback: feedback.trim() },
3411
+ })} /> : null}
3412
+ </View>
3413
+ );
3414
+ }
3415
+
3416
+ const fb: Record<string, ViewStyle & TextStyle> = {
3417
+ input: {
3418
+ minHeight: 104,
3419
+ borderWidth: 1,
3420
+ borderRadius: 12,
3421
+ padding: 12,
3422
+ marginTop: 10,
3423
+ fontSize: 14,
3424
+ lineHeight: 20,
3425
+ } as any,
3426
+ };
3427
+
3428
+ /** One question-family entry point; payload shape selects the smallest native control. */
3429
+ export function QuestionFlowInApp(props: InAppBranchProps): React.ReactElement {
3430
+ const e = ext(props);
3431
+ const variantKey = variantOf(props);
3432
+ if (variantKey === "feedback_capture") {
3433
+ return <FeedbackCaptureInApp {...props} />;
3434
+ }
3435
+ if (variantKey === "emoji_reaction_bar" || arr(e.reactions).length > 0) {
3436
+ return <EmojiReactionBarInApp {...props} />;
3437
+ }
3438
+ if (variantKey === "rating_request" || typeof e.ratingScale === "number") {
3439
+ return <StarRatingInApp {...props} />;
3440
+ }
3441
+ if (
3442
+ variantKey === "nps_prompt" ||
3443
+ typeof e.scaleMax === "number" ||
3444
+ arr(e.scaleLabels).length > 0
3445
+ ) {
3446
+ return <NpsPromptInApp {...props} />;
3447
+ }
3448
+ return <PollCardInApp {...props} />;
3449
+ }
3450
+
3451
+ function GuidedStepsInApp(props: InAppBranchProps): React.ReactElement {
3452
+ const e = ext(props);
3453
+ const raw = arr(e.setupSteps ?? e.steps).slice(0, 12);
3454
+ const steps = raw.length > 0 ? raw : [{ title: props.spec.title, body: props.spec.body }];
3455
+ const [index, setIndex] = useState(0);
3456
+ const [validated, setValidated] = useState<number[]>([]);
3457
+ const step = record(steps[index]);
3458
+ const title = firstText(step, ["title", "label", "heading"]) || props.spec.title;
3459
+ const body = firstText(step, ["body", "message", "description"]) || props.spec.body;
3460
+ const image = mediaUri(step.image ?? step.imageUrl ?? (index === 0 ? props.spec.assets?.image : ""));
3461
+ const mediaHeight = resolveInAppMediaHeight(props.spec, 180, 360);
3462
+ const last = index >= steps.length - 1;
3463
+ const variant = variantOf(props);
3464
+ const validationRules = arr(e.stepValidation);
3465
+ const validation = step.validation ?? validationRules[index] ?? (validationRules.length ? undefined : e.stepValidation);
3466
+ const validationRequired = variant === "guided_setup_wizard" && Boolean(validation);
3467
+ const stepValidated = !validationRequired || validated.includes(index);
3468
+ const advance = (): void => {
3469
+ if (!stepValidated) return;
3470
+ if (!last) {
3471
+ const next = index + 1;
3472
+ setIndex(next);
3473
+ props.onInteraction?.({ mechanic: variant, phase: "progress", progress: next / steps.length, value: next + 1 });
3474
+ return;
3475
+ }
3476
+ props.onInteraction?.({ mechanic: variant, phase: "completed", progress: 1 });
3477
+ if (presentTerminalOutcome(props)) return;
3478
+ if (!props.spec.cta || props.spec.cta.action === "dismiss") props.onDismiss();
3479
+ else props.onCta(props.spec.cta);
3480
+ };
3481
+ return (
3482
+ <View style={sh.content}>
3483
+ <View style={guide.progress} accessibilityRole="progressbar" accessibilityValue={{ min: 1, max: steps.length, now: index + 1 }}>
3484
+ {steps.map((_, stepIndex) => <View key={stepIndex} style={[guide.step, { backgroundColor: stepIndex <= index ? props.theme.accent : props.theme.couponBg }]} />)}
3485
+ </View>
3486
+ {image ? (
3487
+ <Image
3488
+ source={{ uri: image }}
3489
+ resizeMode="cover"
3490
+ style={[
3491
+ guide.image,
3492
+ mediaHeight === undefined ? null : { height: mediaHeight },
3493
+ ] as any}
3494
+ />
3495
+ ) : (
3496
+ <View style={[guide.illustration, { backgroundColor: props.theme.couponBg }]}><Text style={guide.emoji}>{variant === "guided_setup_wizard" ? "⚙️" : "👋"}</Text></View>
3497
+ )}
3498
+ <Title text={title} theme={props.theme} />
3499
+ <Body text={body} theme={props.theme} />
3500
+ {validationRequired ? (
3501
+ <Pressable
3502
+ accessibilityRole="checkbox"
3503
+ accessibilityState={{ checked: stepValidated }}
3504
+ onPress={() => {
3505
+ setValidated((current) => current.includes(index)
3506
+ ? current.filter((value) => value !== index)
3507
+ : [...current, index]);
3508
+ props.onInteraction?.({ mechanic: variant, phase: "decision", value: index + 1, meta: { validation } });
3509
+ }}
3510
+ style={[guide.validation, { borderColor: props.theme.couponBorder }]}
3511
+ >
3512
+ <View style={[guide.validationCheck, { borderColor: stepValidated ? props.theme.accent : props.theme.couponBorder, backgroundColor: stepValidated ? props.theme.accent : props.theme.cardBg }]}>
3513
+ {stepValidated ? <Text style={guide.validationTick}>✓</Text> : null}
3514
+ </View>
3515
+ <Text style={[guide.validationLabel, { color: props.theme.titleColor }]}>{labelOf(validation) || "Mark this step complete"}</Text>
3516
+ </Pressable>
3517
+ ) : null}
3518
+ <View style={guide.actions}>
3519
+ {index > 0 ? <SecondaryBtn label="Back" color={props.theme.secondaryColor} onPress={() => setIndex((value) => Math.max(0, value - 1))} /> : props.spec.secondaryCta ? <SecondaryBtn label={props.spec.secondaryCta.label} color={props.theme.secondaryColor} onPress={props.onDismiss} /> : null}
3520
+ <PrimaryBtn label={last ? props.spec.cta?.label || "Finish" : "Next"} accent={props.theme.accent} onPress={advance} disabled={!stepValidated} />
3521
+ </View>
3522
+ </View>
3523
+ );
3524
+ }
3525
+
3526
+ const guide: Record<string, ViewStyle & TextStyle> = {
3527
+ progress: { flexDirection: "row", gap: 5, marginBottom: 14 } as any,
3528
+ step: { flex: 1, height: 5, borderRadius: 3 } as any,
3529
+ image: { width: "100%", height: 180, borderRadius: 16, marginBottom: 14 } as any,
3530
+ illustration: { height: 150, borderRadius: 16, alignItems: "center", justifyContent: "center", marginBottom: 14 } as any,
3531
+ emoji: { fontSize: 52 } as any,
3532
+ actions: { flexDirection: "row", flexWrap: "wrap", gap: 6, alignItems: "center", marginTop: 14 } as any,
3533
+ validation: { flexDirection: "row", alignItems: "center", gap: 10, borderWidth: 1, borderRadius: 10, padding: 11, marginTop: 10 } as any,
3534
+ validationCheck: { width: 22, height: 22, borderRadius: 6, borderWidth: 1.5, alignItems: "center", justifyContent: "center" } as any,
3535
+ validationTick: { color: "#FFFFFF", fontSize: 12, fontWeight: "900" } as any,
3536
+ validationLabel: { flex: 1, fontSize: 13, fontWeight: "600" } as any,
3537
+ };
3538
+
3539
+ /** Progress-family entry point shared by streak and task-list payloads. */
3540
+ export function ChecklistProgressFamilyInApp(
3541
+ props: InAppBranchProps,
3542
+ ): React.ReactElement {
3543
+ const e = ext(props);
3544
+ const variantKey = variantOf(props);
3545
+ if (variantKey === "first_session_onboarding" || variantKey === "guided_setup_wizard") {
3546
+ return <GuidedStepsInApp {...props} />;
3547
+ }
3548
+ if (variantKey === "community_challenge") {
3549
+ return <CommunityChallengeInApp {...props} />;
3550
+ }
3551
+ if (
3552
+ variantKey === "streak_reward" ||
3553
+ variantKey === "daily_check_in" ||
3554
+ typeof e.filled === "number" ||
3555
+ typeof e.total === "number"
3556
+ ) {
3557
+ return <StreakRewardInApp {...props} />;
3558
+ }
3559
+ if (
3560
+ variantKey === "progress_unlock" ||
3561
+ variantKey === "goal_tracker"
3562
+ ) {
3563
+ return <ProgressSummaryInApp {...props} />;
3564
+ }
3565
+ return <ChecklistProgressInApp {...props} />;
3566
+ }
3567
+
3568
+ // ─── Shared content styles ────────────────────────────────────────────────────
3569
+
3570
+ const sh: Record<string, ViewStyle & TextStyle> = {
3571
+ content: { width: "100%" } as any,
3572
+ title: {
3573
+ fontSize: 18,
3574
+ fontWeight: "800",
3575
+ marginBottom: 6,
3576
+ marginTop: 4,
3577
+ paddingRight: 28,
3578
+ lineHeight: 24,
3579
+ } as any,
3580
+ body: { fontSize: 14, lineHeight: 20, marginBottom: 4 } as any,
3581
+ ctaRow: {
3582
+ flexDirection: "row",
3583
+ flexWrap: "wrap",
3584
+ justifyContent: "flex-end",
3585
+ alignItems: "center",
3586
+ marginTop: 14,
3587
+ gap: 8,
3588
+ } as any,
3589
+ primaryBtn: {
3590
+ paddingVertical: 11,
3591
+ paddingHorizontal: 22,
3592
+ borderRadius: 12,
3593
+ flexBasis: 140,
3594
+ flexGrow: 1,
3595
+ flexShrink: 1,
3596
+ maxWidth: "100%",
3597
+ } as any,
3598
+ primaryText: {
3599
+ fontSize: 15,
3600
+ color: "#fff",
3601
+ fontWeight: "700",
3602
+ flexShrink: 1,
3603
+ textAlign: "center",
3604
+ } as any,
3605
+ secondaryBtn: {
3606
+ flexShrink: 1,
3607
+ maxWidth: "100%",
3608
+ paddingVertical: 11,
3609
+ paddingHorizontal: 14,
3610
+ } as any,
3611
+ secondaryText: {
3612
+ flexShrink: 1,
3613
+ fontSize: 14,
3614
+ fontWeight: "500",
3615
+ textAlign: "center",
3616
+ } as any,
3617
+ };
3618
+
3619
+ // ─── Archetype registry ────────────────────────────────────────────────────────
3620
+
3621
+ export type ArchetypeRenderer = (props: InAppBranchProps) => React.ReactElement;
3622
+
3623
+ export type ArchetypeContentFamily =
3624
+ | "spinWheel"
3625
+ | "scratchReveal"
3626
+ | "slotReward"
3627
+ | "carouselDeck"
3628
+ | "countdown"
3629
+ | "questionFlow"
3630
+ | "checklistProgress"
3631
+ | "qrNfc"
3632
+ | "videoCommerce"
3633
+ | "celebration";
3634
+
3635
+ /** Canonical mechanic families. Null means the container should render the payload. */
3636
+ export const ARCHETYPE_CONTENT_RENDERERS: Readonly<
3637
+ Record<ArchetypeContentFamily, ArchetypeRenderer | null>
3638
+ > = {
3639
+ spinWheel: SpinWheelInApp,
3640
+ scratchReveal: ScratchCardInApp,
3641
+ slotReward: SlotRewardInApp,
3642
+ carouselDeck: CarouselDeckInApp,
3643
+ countdown: CountdownInApp,
3644
+ questionFlow: QuestionFlowInApp,
3645
+ checklistProgress: ChecklistProgressFamilyInApp,
3646
+ qrNfc: QrNfcInApp,
3647
+ videoCommerce: VideoCommerceInApp,
3648
+ celebration: CelebrationInApp,
3649
+ };
3650
+
3651
+ /** Resolve a canonical family key without silently inventing missing mechanics. */
3652
+ export function resolveArchetypeContent(
3653
+ archetype?: string | null,
3654
+ ): ArchetypeRenderer | null {
3655
+ if (
3656
+ !archetype ||
3657
+ !Object.prototype.hasOwnProperty.call(
3658
+ ARCHETYPE_CONTENT_RENDERERS,
3659
+ archetype,
3660
+ )
3661
+ )
3662
+ return null;
3663
+ return ARCHETYPE_CONTENT_RENDERERS[archetype as ArchetypeContentFamily];
3664
+ }
3665
+
3666
+ export const ARCHETYPE_RENDERERS: Record<string, ArchetypeRenderer> = {
3667
+ // Gamified
3668
+ spin_wheel: SpinWheelInApp,
3669
+ scratch_card: ScratchCardInApp,
3670
+ slot_reward: SlotRewardInApp,
3671
+ carousel_story: CarouselDeckInApp,
3672
+ swipe_deck: CarouselDeckInApp,
3673
+ countdown_offer: CountdownInApp,
3674
+ trial_conversion: CountdownInApp,
3675
+ // Feedback / questions
3676
+ poll_card: PollCardInApp,
3677
+ survey_sheet: QuestionFlowInApp,
3678
+ quiz_card: QuestionFlowInApp,
3679
+ product_finder: QuestionFlowInApp,
3680
+ preferences_picker: QuestionFlowInApp,
3681
+ feedback_capture: QuestionFlowInApp,
3682
+ nps_prompt: NpsPromptInApp,
3683
+ emoji_reaction_bar: EmojiReactionBarInApp,
3684
+ rating_request: StarRatingInApp,
3685
+ app_review_ask: StarRatingInApp,
3686
+ // Engagement / lifecycle
3687
+ streak_reward: StreakRewardInApp,
3688
+ daily_check_in: StreakRewardInApp,
3689
+ interactive_checklist: ChecklistProgressInApp,
3690
+ guided_setup_wizard: ChecklistProgressFamilyInApp,
3691
+ checklist_progress: ChecklistProgressInApp,
3692
+ progress_unlock: ChecklistProgressFamilyInApp,
3693
+ goal_tracker: ChecklistProgressFamilyInApp,
3694
+ first_session_onboarding: ChecklistProgressFamilyInApp,
3695
+ community_challenge: ChecklistProgressFamilyInApp,
3696
+ // Commerce / offers
3697
+ coupon_wallet: CouponWalletInApp,
3698
+ exit_intent_rescue: CouponWalletInApp,
3699
+ nfc_qr_prompt: QrNfcInApp,
3700
+ wallet_pass_offer: QrNfcInApp,
3701
+ video_commerce_card: VideoCommerceInApp,
3702
+ milestone_celebration: CelebrationInApp,
3703
+ celebration_confetti_full_screen: CelebrationInApp,
3704
+ };
3705
+