react-native-glance-chat 0.0.0-stage → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GlanceChat.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +50 -2
- package/android/build.gradle +104 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/glancechat/GlanceChatModuleImpl.kt +174 -0
- package/android/src/main/java/com/glancechat/GlanceChatOverlayHostView.kt +39 -0
- package/android/src/main/java/com/glancechat/GlanceChatOverlayViewManager.kt +74 -0
- package/android/src/main/java/com/glancechat/GlanceChatPackage.kt +40 -0
- package/android/src/main/java/com/glancechat/GlanceMixAndMatchWidgetViewManager.kt +142 -0
- package/android/src/main/java/com/glancechat/GlancePdpSuggestionsViewManager.kt +40 -0
- package/android/src/main/java/com/glancechat/GlanceProductTryOnWidgetViewManager.kt +145 -0
- package/android/src/main/java/com/glancechat/UrlBrandLogoProvider.kt +24 -0
- package/android/src/newarch/java/com/glancechat/GlanceChatModule.kt +44 -0
- package/android/src/oldarch/java/com/glancechat/GlanceChatModule.kt +59 -0
- package/ios/GlanceChat.h +5 -0
- package/ios/GlanceChat.mm +21 -0
- package/lib/module/GlanceChatOverlayNativeComponent.ts +22 -0
- package/lib/module/GlanceMixAndMatchWidgetNativeComponent.ts +21 -0
- package/lib/module/GlancePdpSuggestionsNativeComponent.ts +9 -0
- package/lib/module/GlanceProductTryOnWidgetNativeComponent.ts +18 -0
- package/lib/module/NativeGlanceChat.js +20 -0
- package/lib/module/NativeGlanceChat.js.map +1 -0
- package/lib/module/index.js +65 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/GlanceChatOverlayNativeComponent.d.ts +18 -0
- package/lib/typescript/src/GlanceChatOverlayNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/GlanceMixAndMatchWidgetNativeComponent.d.ts +17 -0
- package/lib/typescript/src/GlanceMixAndMatchWidgetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/GlancePdpSuggestionsNativeComponent.d.ts +6 -0
- package/lib/typescript/src/GlancePdpSuggestionsNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/GlanceProductTryOnWidgetNativeComponent.d.ts +14 -0
- package/lib/typescript/src/GlanceProductTryOnWidgetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeGlanceChat.d.ts +79 -0
- package/lib/typescript/src/NativeGlanceChat.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +126 -4
- package/src/GlanceChatOverlayNativeComponent.ts +22 -0
- package/src/GlanceMixAndMatchWidgetNativeComponent.ts +21 -0
- package/src/GlancePdpSuggestionsNativeComponent.ts +9 -0
- package/src/GlanceProductTryOnWidgetNativeComponent.ts +18 -0
- package/src/NativeGlanceChat.ts +96 -0
- 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
|
-
#
|
|
1
|
+
# react-native-glance-chat
|
|
2
2
|
|
|
3
|
-
|
|
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.10"
|
|
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,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
|
+
}
|