react-native-glance-chat 0.0.0-stage → 0.1.3

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 (46) hide show
  1. package/GlanceChat.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +50 -2
  4. package/android/build.gradle +104 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/glancechat/GlanceChatModuleImpl.kt +174 -0
  7. package/android/src/main/java/com/glancechat/GlanceChatOverlayHostView.kt +39 -0
  8. package/android/src/main/java/com/glancechat/GlanceChatOverlayViewManager.kt +74 -0
  9. package/android/src/main/java/com/glancechat/GlanceChatPackage.kt +40 -0
  10. package/android/src/main/java/com/glancechat/GlanceMixAndMatchWidgetViewManager.kt +142 -0
  11. package/android/src/main/java/com/glancechat/GlancePdpSuggestionsViewManager.kt +40 -0
  12. package/android/src/main/java/com/glancechat/GlanceProductTryOnWidgetViewManager.kt +145 -0
  13. package/android/src/main/java/com/glancechat/UrlBrandLogoProvider.kt +24 -0
  14. package/android/src/newarch/java/com/glancechat/GlanceChatModule.kt +44 -0
  15. package/android/src/oldarch/java/com/glancechat/GlanceChatModule.kt +59 -0
  16. package/ios/GlanceChat.h +5 -0
  17. package/ios/GlanceChat.mm +21 -0
  18. package/lib/module/GlanceChatOverlayNativeComponent.ts +22 -0
  19. package/lib/module/GlanceMixAndMatchWidgetNativeComponent.ts +21 -0
  20. package/lib/module/GlancePdpSuggestionsNativeComponent.ts +9 -0
  21. package/lib/module/GlanceProductTryOnWidgetNativeComponent.ts +18 -0
  22. package/lib/module/NativeGlanceChat.js +20 -0
  23. package/lib/module/NativeGlanceChat.js.map +1 -0
  24. package/lib/module/index.js +65 -0
  25. package/lib/module/index.js.map +1 -0
  26. package/lib/module/package.json +1 -0
  27. package/lib/typescript/package.json +1 -0
  28. package/lib/typescript/src/GlanceChatOverlayNativeComponent.d.ts +18 -0
  29. package/lib/typescript/src/GlanceChatOverlayNativeComponent.d.ts.map +1 -0
  30. package/lib/typescript/src/GlanceMixAndMatchWidgetNativeComponent.d.ts +17 -0
  31. package/lib/typescript/src/GlanceMixAndMatchWidgetNativeComponent.d.ts.map +1 -0
  32. package/lib/typescript/src/GlancePdpSuggestionsNativeComponent.d.ts +6 -0
  33. package/lib/typescript/src/GlancePdpSuggestionsNativeComponent.d.ts.map +1 -0
  34. package/lib/typescript/src/GlanceProductTryOnWidgetNativeComponent.d.ts +14 -0
  35. package/lib/typescript/src/GlanceProductTryOnWidgetNativeComponent.d.ts.map +1 -0
  36. package/lib/typescript/src/NativeGlanceChat.d.ts +79 -0
  37. package/lib/typescript/src/NativeGlanceChat.d.ts.map +1 -0
  38. package/lib/typescript/src/index.d.ts +34 -0
  39. package/lib/typescript/src/index.d.ts.map +1 -0
  40. package/package.json +126 -4
  41. package/src/GlanceChatOverlayNativeComponent.ts +22 -0
  42. package/src/GlanceMixAndMatchWidgetNativeComponent.ts +21 -0
  43. package/src/GlancePdpSuggestionsNativeComponent.ts +9 -0
  44. package/src/GlanceProductTryOnWidgetNativeComponent.ts +18 -0
  45. package/src/NativeGlanceChat.ts +96 -0
  46. package/src/index.tsx +78 -0
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "GlanceChat"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/inmobi-glance/react-native-glance-chat.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+ end
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Glance
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # react-native-glance-chat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React Native wrapper for the Glance Chat SDK (Android + iOS), exposing the SDK's
4
+ helper methods and UI components to React Native apps.
5
+
6
+ Backward compatible with both the React Native New Architecture (Turbo Modules / Fabric,
7
+ RN 0.76+) and the legacy bridge.
8
+
9
+ > Integration design reference: see `docs/REACT_NATIVE_INTEGRATION.md` in the
10
+ > [Helios-Android](https://github.com/inmobi-glance/Helios-Android) SDK repo.
11
+
12
+ ## Status
13
+
14
+ Initial scaffold via `create-react-native-library` (Turbo module + Kotlin/Objective-C).
15
+ The generated `multiply` placeholder will be replaced by the Glance Chat API
16
+ (`init`, `setPageContext`, `closeChat`, …) and UI components (`GlanceChatOverlay`,
17
+ `ProductTryOnWidget`, `PdpSuggestionsWidget`). Implementation in progress.
18
+
19
+ ## Installation
20
+
21
+
22
+ ```sh
23
+ npm install react-native-glance-chat
24
+ ```
25
+
26
+
27
+ ## Usage
28
+
29
+
30
+ ```js
31
+ import { multiply } from 'react-native-glance-chat';
32
+
33
+ // ...
34
+
35
+ const result = multiply(3, 7);
36
+ ```
37
+
38
+
39
+ ## Contributing
40
+
41
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
42
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
43
+ - [Code of conduct](CODE_OF_CONDUCT.md)
44
+
45
+ ## License
46
+
47
+ MIT
48
+
49
+ ---
50
+
51
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,104 @@
1
+ buildscript {
2
+ ext.GlanceChat = [
3
+ kotlinVersion: "2.0.21",
4
+ // Floor raised 24→26: glance-chat-sdk 1.0.4+ (Snapshot tracker) requires minSdk 26.
5
+ minSdkVersion: 26,
6
+ compileSdkVersion: 35,
7
+ // Version of the native Glance Chat SDK (com.glance.chat:glance-chat-sdk) to depend on.
8
+ // Released versions resolve from Cloudsmith (glance/glance_telco_sdk) — see
9
+ // example/android/settings.gradle and the README integration section. During local
10
+ // development, publish with `./gradlew publishToMavenLocal -Psdk.version=1.0.0`
11
+ // (produces 1.0.0-local) and set sdkVersion to "1.0.0-local" to resolve from mavenLocal().
12
+ sdkVersion: "1.0.9"
13
+ ]
14
+
15
+ ext.getExtOrDefault = { prop ->
16
+ if (rootProject.ext.has(prop)) {
17
+ return rootProject.ext.get(prop)
18
+ }
19
+
20
+ return GlanceChat[prop]
21
+ }
22
+
23
+ repositories {
24
+ google()
25
+ mavenCentral()
26
+ }
27
+
28
+ dependencies {
29
+ classpath "com.android.tools.build:gradle:8.7.2"
30
+ // noinspection DifferentKotlinGradleVersion
31
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
32
+ // Compose compiler plugin (Kotlin 2.0+) — required to host the SDK's Composables
33
+ // in ComposeView from the view managers.
34
+ classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
35
+ }
36
+ }
37
+
38
+
39
+ apply plugin: "com.android.library"
40
+ apply plugin: "kotlin-android"
41
+ apply plugin: "org.jetbrains.kotlin.plugin.compose"
42
+
43
+ apply plugin: "com.facebook.react"
44
+
45
+ def isNewArchitectureEnabled() {
46
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
47
+ }
48
+
49
+ android {
50
+ namespace "com.glancechat"
51
+
52
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
53
+
54
+ defaultConfig {
55
+ minSdkVersion getExtOrDefault("minSdkVersion")
56
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
57
+ // Logged at init so logcat can confirm which SDK AAR version was consumed.
58
+ buildConfigField "String", "SDK_VERSION", "\"${getExtOrDefault('sdkVersion')}\""
59
+ }
60
+
61
+ buildFeatures {
62
+ buildConfig true
63
+ compose true
64
+ }
65
+
66
+ compileOptions {
67
+ sourceCompatibility JavaVersion.VERSION_17
68
+ targetCompatibility JavaVersion.VERSION_17
69
+ }
70
+
71
+ // Backward compatibility: compile the New Architecture module wrapper (extends the
72
+ // codegen-generated NativeGlanceChatSpec) or the legacy bridge wrapper depending on
73
+ // the consumer app's newArchEnabled flag. Shared logic lives in src/main.
74
+ sourceSets {
75
+ main {
76
+ if (isNewArchitectureEnabled()) {
77
+ java.srcDirs += ["src/newarch"]
78
+ } else {
79
+ java.srcDirs += ["src/oldarch"]
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ dependencies {
86
+ implementation "com.facebook.react:react-android"
87
+
88
+ // Jetpack Compose — the view managers host the SDK's Composables in a ComposeView.
89
+ def composeBom = platform("androidx.compose:compose-bom:2024.02.00")
90
+ implementation composeBom
91
+ implementation "androidx.compose.ui:ui"
92
+ implementation "androidx.activity:activity-compose:1.8.2"
93
+ // Coil for UrlBrandLogoProvider (AsyncImage).
94
+ implementation "io.coil-kt:coil-compose:2.5.0"
95
+
96
+ // Native Glance Chat SDK. The root artifact pulls the feature modules
97
+ // (core/tryon/chat/onboarding/mixandmatch) transitively via its POM.
98
+ //
99
+ // Resolution happens through the consuming app's settings-level repositories
100
+ // (react-native-test-app uses FAIL_ON_PROJECT_REPOS), so the consuming app must declare
101
+ // the Cloudsmith repo with credentials in its settings.gradle — see the README
102
+ // integration section. The example also adds mavenLocal() for local development.
103
+ implementation "com.glance.chat:glance-chat-sdk:${getExtOrDefault('sdkVersion')}"
104
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,174 @@
1
+ package com.glancechat
2
+
3
+ import android.net.Uri
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import android.provider.Settings
7
+ import android.util.Log
8
+ import com.facebook.react.bridge.Arguments
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.bridge.ReadableMap
11
+ import com.facebook.react.modules.core.DeviceEventManagerModule
12
+ import com.glance.chat.GlanceChatSDK
13
+ import com.glance.chat.core.config.GlanceChatConfig
14
+ import kotlinx.coroutines.CoroutineScope
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.SupervisorJob
17
+ import kotlinx.coroutines.cancel
18
+ import kotlinx.coroutines.launch
19
+
20
+ /**
21
+ * Architecture-agnostic implementation of the Glance Chat native module.
22
+ *
23
+ * All delegation to the native Glance Chat SDK lives here so that the thin
24
+ * New Architecture ([NativeGlanceChatSpec] subclass) and legacy bridge
25
+ * ([com.facebook.react.bridge.ReactContextBaseJavaModule] subclass) wrappers
26
+ * can share one implementation. See docs/REACT_NATIVE_INTEGRATION.md §10 in the
27
+ * Helios-Android repo.
28
+ *
29
+ * Events are delivered uniformly through `RCTDeviceEventEmitter` (same event names
30
+ * the JS `Glance.onX` subscriptions listen for), which works identically for both
31
+ * architectures.
32
+ */
33
+ class GlanceChatModuleImpl(private val reactContext: ReactApplicationContext) {
34
+
35
+ private val mainHandler = Handler(Looper.getMainLooper())
36
+ private var scope: CoroutineScope? = null
37
+
38
+ fun initialize(partnerId: String, config: ReadableMap, userId: String?) {
39
+ Log.i(
40
+ TAG,
41
+ "Glance Chat native SDK loaded: com.glance.chat:glance-chat-sdk:${BuildConfig.SDK_VERSION} " +
42
+ "(newArch=${BuildConfig.IS_NEW_ARCHITECTURE_ENABLED}, partnerId=$partnerId)"
43
+ )
44
+ val cfg = buildConfig(partnerId, userId, config)
45
+ runOnMain {
46
+ GlanceChatSDK.init(reactContext.applicationContext, cfg)
47
+
48
+ // Product taps come to JS: the host routes navigation in the RN layer, not via
49
+ // native deeplink Intents. Emits onProductClick always, onDeeplinkClick when the
50
+ // backend supplied a deeplink on the product.
51
+ GlanceChatSDK.productNavigationHandler = { product ->
52
+ emit(
53
+ EVENT_PRODUCT_CLICK,
54
+ Arguments.createMap().apply {
55
+ putString("productId", product.id)
56
+ putString("name", product.name)
57
+ }
58
+ )
59
+ product.deeplinkUrl.takeIf { it.isNotBlank() }?.let { emit(EVENT_DEEPLINK_CLICK, it) }
60
+ }
61
+
62
+ // Auth token cannot be fetched/refreshed — host should re-authenticate.
63
+ GlanceChatSDK.onSessionExpired = {
64
+ emit(EVENT_SESSION_EXPIRED, Arguments.createMap())
65
+ }
66
+
67
+ observeTryOnStates()
68
+ }
69
+ }
70
+
71
+ fun destroy() {
72
+ runOnMain {
73
+ scope?.cancel()
74
+ scope = null
75
+ GlanceChatSDK.productNavigationHandler = null
76
+ GlanceChatSDK.onSessionExpired = null
77
+ GlanceChatSDK.destroy()
78
+ }
79
+ }
80
+
81
+ fun isInitialized(): Boolean = GlanceChatSDK.isInitialized
82
+
83
+ fun isLoggedIn(): Boolean = GlanceChatSDK.isLoggedIn
84
+
85
+ fun setPageContext(context: String, metadata: ReadableMap) {
86
+ GlanceChatSDK.setPageContext(context, metadata.toStringMap())
87
+ }
88
+
89
+ fun clearPageContext() = GlanceChatSDK.clearPageContext()
90
+
91
+ fun closeChat() = runOnMain { GlanceChatSDK.closeChat() }
92
+
93
+ fun openChatWithSuggestion(suggestion: String) =
94
+ runOnMain { GlanceChatSDK.openChatWithSuggestion(suggestion) }
95
+
96
+ fun handleDeepLink(uri: String): Boolean =
97
+ GlanceChatSDK.handleDeepLink(Uri.parse(uri))
98
+
99
+ fun startOnboarding(variantId: String?) =
100
+ runOnMain { GlanceChatSDK.startOnboarding(variantId) }
101
+
102
+ // --- Internal helpers ----------------------------------------------------
103
+
104
+ private fun buildConfig(partnerId: String, userId: String?, config: ReadableMap): GlanceChatConfig {
105
+ // GlanceChatConfig.Builder requires a non-blank userId (it's exchanged for the SDK's
106
+ // auth token). Host apps that haven't logged the user in yet (e.g. this dev harness)
107
+ // pass null/blank — fall back to a stable per-device id so init still succeeds; the
108
+ // user remains "not onboarded" (GlanceChatSDK.isLoggedInFlow) until they complete the
109
+ // SDK's own onboarding, independent of this identifier.
110
+ val resolvedUserId = userId?.takeIf { it.isNotBlank() } ?: androidId()
111
+ return GlanceChatConfig.Builder()
112
+ .partnerId(partnerId)
113
+ .userId(resolvedUserId)
114
+ .environment(parseEnvironment(config.getString("environment")))
115
+ .build()
116
+ }
117
+
118
+ @Suppress("HardwareIds")
119
+ private fun androidId(): String =
120
+ Settings.Secure.getString(reactContext.contentResolver, Settings.Secure.ANDROID_ID) ?: "anonymous"
121
+
122
+ private fun parseEnvironment(value: String?): GlanceChatConfig.Environment =
123
+ when (value?.uppercase()) {
124
+ "UAT" -> GlanceChatConfig.Environment.UAT
125
+ "PRODUCTION" -> GlanceChatConfig.Environment.PRODUCTION
126
+ else -> GlanceChatConfig.Environment.DEVELOPMENT
127
+ }
128
+
129
+ private fun observeTryOnStates() {
130
+ scope?.cancel()
131
+ val newScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
132
+ scope = newScope
133
+ newScope.launch {
134
+ GlanceChatSDK.tryOnStates.collect { states ->
135
+ val statesMap = Arguments.createMap()
136
+ states.forEach { (variantId, state) -> statesMap.putString(variantId, state.status.name) }
137
+ val payload = Arguments.createMap().apply { putMap("states", statesMap) }
138
+ emit(EVENT_TRY_ON_STATES, payload)
139
+ }
140
+ }
141
+ }
142
+
143
+ private fun emit(eventName: String, data: Any?) {
144
+ if (!reactContext.hasActiveReactInstance()) return
145
+ reactContext
146
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
147
+ .emit(eventName, data)
148
+ }
149
+
150
+ private fun runOnMain(block: () -> Unit) {
151
+ if (Looper.myLooper() == Looper.getMainLooper()) block() else mainHandler.post(block)
152
+ }
153
+
154
+ companion object {
155
+ const val NAME = "GlanceChat"
156
+ const val TAG = "GlanceChatRN"
157
+
158
+ private const val EVENT_TRY_ON_STATES = "onTryOnStatesChanged"
159
+ private const val EVENT_PRODUCT_CLICK = "onProductClick"
160
+ private const val EVENT_DEEPLINK_CLICK = "onDeeplinkClick"
161
+ private const val EVENT_SESSION_EXPIRED = "onSessionExpired"
162
+ }
163
+ }
164
+
165
+ /** Convert a [ReadableMap] of string entries into a Kotlin `Map<String, String>`. */
166
+ private fun ReadableMap.toStringMap(): Map<String, String> {
167
+ val result = mutableMapOf<String, String>()
168
+ val iterator = keySetIterator()
169
+ while (iterator.hasNextKey()) {
170
+ val key = iterator.nextKey()
171
+ getString(key)?.let { result[key] = it }
172
+ }
173
+ return result
174
+ }
@@ -0,0 +1,39 @@
1
+ package com.glancechat
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+ import android.widget.FrameLayout
6
+ import androidx.compose.runtime.Composable
7
+ import androidx.compose.ui.platform.ComposeView
8
+ import androidx.compose.ui.platform.ViewCompositionStrategy
9
+ import com.facebook.react.uimanager.PointerEvents
10
+ import com.facebook.react.uimanager.ReactPointerEventsView
11
+
12
+ /**
13
+ * [FrameLayout] wrapping a [ComposeView] (which is `final` in Compose UI and can't be
14
+ * subclassed directly) that implements [ReactPointerEventsView] so RN's Fabric hit-testing
15
+ * (TouchTargetHelper) actually honors pointer-events for this component. A plain [ComposeView]
16
+ * doesn't implement that interface, so RN always treats it as [PointerEvents.AUTO] no matter
17
+ * what JS sets — see docs/REACT_NATIVE_INTEGRATION.md §5 for the investigation. This view's
18
+ * [pointerEvents] is driven natively from [GlanceChatOverlayViewManager] based on whether the
19
+ * hosted Compose content actually has anything visible to show, so the overlay can sit at a
20
+ * permanently high RN zIndex without ever stealing touches from sibling views while empty.
21
+ */
22
+ class GlanceChatOverlayHostView(context: Context) : FrameLayout(context), ReactPointerEventsView {
23
+ override var pointerEvents: PointerEvents = PointerEvents.NONE
24
+
25
+ private val composeView = ComposeView(context).also {
26
+ addView(
27
+ it,
28
+ ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
29
+ )
30
+ }
31
+
32
+ fun setViewCompositionStrategy(strategy: ViewCompositionStrategy) {
33
+ composeView.setViewCompositionStrategy(strategy)
34
+ }
35
+
36
+ fun setContent(content: @Composable () -> Unit) {
37
+ composeView.setContent(content)
38
+ }
39
+ }
@@ -0,0 +1,74 @@
1
+ package com.glancechat
2
+
3
+ import androidx.compose.ui.platform.ViewCompositionStrategy
4
+ import com.facebook.react.module.annotations.ReactModule
5
+ import com.facebook.react.uimanager.PointerEvents
6
+ import com.facebook.react.uimanager.SimpleViewManager
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+ import com.facebook.react.uimanager.ViewManagerDelegate
9
+ import com.facebook.react.viewmanagers.GlanceChatOverlayViewManagerDelegate
10
+ import com.facebook.react.viewmanagers.GlanceChatOverlayViewManagerInterface
11
+ import com.glance.chat.ui.components.GlanceChatOverlay
12
+
13
+ /**
14
+ * Fabric/legacy view manager that hosts the SDK's [GlanceChatOverlay] Composable inside a
15
+ * [GlanceChatOverlayHostView]. Implements the codegen-generated
16
+ * [GlanceChatOverlayViewManagerInterface] and routes prop updates through the generated delegate
17
+ * so props propagate under the New Architecture (Fabric). See docs/REACT_NATIVE_INTEGRATION.md §5.
18
+ *
19
+ * The host view's [PointerEvents] are driven natively (not by a JS `pointerEvents` prop — RN's
20
+ * hit-testing only honors that for views implementing [com.facebook.react.uimanager.ReactPointerEventsView],
21
+ * which a plain ComposeView doesn't, and even then a single opaque Compose child defeats
22
+ * "box-none" passthrough) based on whether the hosted Compose content actually has anything
23
+ * visible to show. This lets the overlay sit at a permanently high RN zIndex (drawing over
24
+ * everything, e.g. onboarding/chat UI) while never stealing touches from sibling views — like an
25
+ * always-mounted dev/debug panel's own buttons — while it's rendering nothing.
26
+ */
27
+ @ReactModule(name = GlanceChatOverlayViewManager.NAME)
28
+ class GlanceChatOverlayViewManager :
29
+ SimpleViewManager<GlanceChatOverlayHostView>(),
30
+ GlanceChatOverlayViewManagerInterface<GlanceChatOverlayHostView> {
31
+
32
+ private val delegate = GlanceChatOverlayViewManagerDelegate(this)
33
+
34
+ private var brandLogoUrl: String? = null
35
+ private var chatEnabled: Boolean = true
36
+
37
+ override fun getName() = NAME
38
+
39
+ override fun getDelegate(): ViewManagerDelegate<GlanceChatOverlayHostView> = delegate
40
+
41
+ override fun createViewInstance(context: ThemedReactContext): GlanceChatOverlayHostView {
42
+ return GlanceChatOverlayHostView(context).apply {
43
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
44
+ renderContent(this)
45
+ }
46
+ }
47
+
48
+ override fun setBrandLogoUrl(view: GlanceChatOverlayHostView, value: String?) {
49
+ brandLogoUrl = value
50
+ renderContent(view)
51
+ }
52
+
53
+ override fun setChatEnabled(view: GlanceChatOverlayHostView, value: Boolean) {
54
+ chatEnabled = value
55
+ renderContent(view)
56
+ }
57
+
58
+ private fun renderContent(view: GlanceChatOverlayHostView) {
59
+ val logoUrl = brandLogoUrl
60
+ view.setContent {
61
+ GlanceChatOverlay(
62
+ brandLogoProvider = logoUrl?.let { UrlBrandLogoProvider(it) },
63
+ onContentVisibilityChanged = { hasContent ->
64
+ view.pointerEvents = if (hasContent) PointerEvents.AUTO else PointerEvents.NONE
65
+ },
66
+ chatEnabled = chatEnabled
67
+ )
68
+ }
69
+ }
70
+
71
+ companion object {
72
+ const val NAME = "GlanceChatOverlayView"
73
+ }
74
+ }
@@ -0,0 +1,40 @@
1
+ package com.glancechat
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ class GlanceChatPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == GlanceChatModuleImpl.NAME) {
13
+ GlanceChatModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun createViewManagers(
20
+ reactContext: ReactApplicationContext
21
+ ): List<ViewManager<*, *>> = listOf(
22
+ GlanceChatOverlayViewManager(),
23
+ GlanceProductTryOnWidgetViewManager(),
24
+ GlanceMixAndMatchWidgetViewManager(),
25
+ GlancePdpSuggestionsViewManager()
26
+ )
27
+
28
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
29
+ mapOf(
30
+ GlanceChatModuleImpl.NAME to ReactModuleInfo(
31
+ name = GlanceChatModuleImpl.NAME,
32
+ className = GlanceChatModuleImpl.NAME,
33
+ canOverrideExistingModule = false,
34
+ needsEagerInit = false,
35
+ isCxxModule = false,
36
+ isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
37
+ )
38
+ )
39
+ }
40
+ }
@@ -0,0 +1,142 @@
1
+ package com.glancechat
2
+
3
+ import android.widget.FrameLayout
4
+ import androidx.compose.ui.platform.ComposeView
5
+ import androidx.compose.ui.platform.ViewCompositionStrategy
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.facebook.react.module.annotations.ReactModule
8
+ import com.facebook.react.uimanager.SimpleViewManager
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import com.facebook.react.uimanager.UIManagerHelper
11
+ import com.facebook.react.uimanager.ViewManagerDelegate
12
+ import com.facebook.react.uimanager.events.Event
13
+ import com.facebook.react.viewmanagers.GlanceMixAndMatchWidgetViewManagerDelegate
14
+ import com.facebook.react.viewmanagers.GlanceMixAndMatchWidgetViewManagerInterface
15
+ import com.glance.chat.ui.components.MixAndMatchWidget
16
+
17
+ /**
18
+ * Fabric/legacy view manager hosting the SDK's [MixAndMatchWidget] Composable (the
19
+ * complete-the-look tray with try-on and cart sheets). Implements the codegen-generated
20
+ * interface so `variantId` / `enabled` props route through the delegate, and forwards the
21
+ * widget's add-to-cart callback to the JS `onAddToCartClick` bubbling event.
22
+ *
23
+ * Composition is deferred to window attach via [WidgetHostLayout] — same reason as
24
+ * GlanceProductTryOnWidgetViewManager (Fabric measures before attach; bare ComposeView
25
+ * throws "Cannot locate windowRecomposer").
26
+ */
27
+ @ReactModule(name = GlanceMixAndMatchWidgetViewManager.NAME)
28
+ class GlanceMixAndMatchWidgetViewManager :
29
+ SimpleViewManager<FrameLayout>(),
30
+ GlanceMixAndMatchWidgetViewManagerInterface<FrameLayout> {
31
+
32
+ private val delegate = GlanceMixAndMatchWidgetViewManagerDelegate(this)
33
+
34
+ override fun getName() = NAME
35
+
36
+ override fun getDelegate(): ViewManagerDelegate<FrameLayout> = delegate
37
+
38
+ override fun createViewInstance(context: ThemedReactContext): FrameLayout =
39
+ WidgetHostLayout(context)
40
+
41
+ override fun setVariantId(view: FrameLayout, value: String?) {
42
+ (view as WidgetHostLayout).variantId = value ?: ""
43
+ }
44
+
45
+ override fun setEnabled(view: FrameLayout, value: Boolean) {
46
+ (view as WidgetHostLayout).widgetEnabled = value
47
+ }
48
+
49
+ override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
50
+ return mapOf(
51
+ "onAddToCartClick" to mapOf(
52
+ "phasedRegistrationNames" to mapOf(
53
+ "bubbled" to "onAddToCartClick",
54
+ "captured" to "onAddToCartClickCapture"
55
+ )
56
+ )
57
+ )
58
+ }
59
+
60
+ private class AddToCartEvent(
61
+ surfaceId: Int,
62
+ viewId: Int,
63
+ private val productId: String,
64
+ private val size: String
65
+ ) : Event<AddToCartEvent>(surfaceId, viewId) {
66
+ override fun getEventName() = "onAddToCartClick"
67
+ override fun getEventData() = Arguments.createMap().apply {
68
+ putString("productId", productId)
69
+ putString("size", size)
70
+ }
71
+ }
72
+
73
+ private class WidgetHostLayout(context: ThemedReactContext) : FrameLayout(context) {
74
+ private var composeView: ComposeView? = null
75
+
76
+ var variantId: String = ""
77
+ set(value) {
78
+ field = value
79
+ if (isAttachedToWindow) renderContent()
80
+ }
81
+
82
+ var widgetEnabled: Boolean = true
83
+ set(value) {
84
+ field = value
85
+ if (isAttachedToWindow) renderContent()
86
+ }
87
+
88
+ override fun onAttachedToWindow() {
89
+ super.onAttachedToWindow()
90
+ renderContent()
91
+ }
92
+
93
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
94
+ super.onLayout(changed, l, t, r, b)
95
+ layoutChild()
96
+ }
97
+
98
+ private fun renderContent() {
99
+ val view = composeView ?: ComposeView(context).also {
100
+ it.setViewCompositionStrategy(
101
+ ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool
102
+ )
103
+ addView(
104
+ it,
105
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
106
+ )
107
+ composeView = it
108
+ }
109
+ val currentVariantId = variantId
110
+ view.setContent {
111
+ MixAndMatchWidget(
112
+ merchantVariantId = currentVariantId,
113
+ onAddToCartClick = { product, size ->
114
+ emitAddToCart(product.id, size.value)
115
+ }
116
+ )
117
+ }
118
+ layoutChild()
119
+ }
120
+
121
+ private fun layoutChild() {
122
+ val child = composeView ?: return
123
+ if (width == 0 || height == 0) return
124
+ child.measure(
125
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
126
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
127
+ )
128
+ child.layout(0, 0, width, height)
129
+ }
130
+
131
+ private fun emitAddToCart(productId: String, size: String) {
132
+ val reactContext = context as? ThemedReactContext ?: return
133
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
134
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
135
+ dispatcher?.dispatchEvent(AddToCartEvent(surfaceId, id, productId, size))
136
+ }
137
+ }
138
+
139
+ companion object {
140
+ const val NAME = "GlanceMixAndMatchWidgetView"
141
+ }
142
+ }