@octopus-community/react-native 1.0.7 → 1.9.1
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/OctopusReactNativeSdk.podspec +1 -1
- package/README.md +40 -35
- package/android/build.gradle +2 -0
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/AndroidManifestNew.xml +2 -1
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusActivity.kt +56 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusContent.kt +396 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventEmitter.kt +22 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventSerializer.kt +339 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactModule.kt +326 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/{OctopusReactNativeSdkPackage.kt → OctopusReactPackage.kt} +3 -3
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +53 -9
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +5 -15
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfiguration.kt +6 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfigurationManager.kt +12 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt +17 -2
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIViewManager.kt +63 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt +2 -2
- package/ios/OctopusEventManager.swift +27 -0
- package/ios/OctopusEventSerializer.swift +271 -0
- package/ios/OctopusReactNativeSdk.mm +26 -1
- package/ios/OctopusReactNativeSdk.swift +225 -3
- package/ios/OctopusSDKInitializer.swift +32 -0
- package/ios/OctopusSSOAuthenticator.swift +1 -5
- package/ios/OctopusUIConfiguration.swift +6 -0
- package/ios/OctopusUIManager.swift +134 -10
- package/ios/OctopusUIViewManager.m +7 -0
- package/ios/OctopusUIViewManager.swift +37 -0
- package/lib/module/OctopusUIView.js +39 -0
- package/lib/module/OctopusUIView.js.map +1 -0
- package/lib/module/addHasAccessToCommunityListener.js +33 -0
- package/lib/module/addHasAccessToCommunityListener.js.map +1 -0
- package/lib/module/addNavigateToUrlListener.js +41 -0
- package/lib/module/addNavigateToUrlListener.js.map +1 -0
- package/lib/module/addNotSeenNotificationsCountListener.js +30 -0
- package/lib/module/addNotSeenNotificationsCountListener.js.map +1 -0
- package/lib/module/addSDKEventListener.js +48 -0
- package/lib/module/addSDKEventListener.js.map +1 -0
- package/lib/module/connectUser.js +24 -3
- package/lib/module/connectUser.js.map +1 -1
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/initialize.js +13 -12
- package/lib/module/initialize.js.map +1 -1
- package/lib/module/openUI.js +23 -2
- package/lib/module/openUI.js.map +1 -1
- package/lib/module/overrideCommunityAccess.js +36 -0
- package/lib/module/overrideCommunityAccess.js.map +1 -0
- package/lib/module/overrideDefaultLocale.js +75 -0
- package/lib/module/overrideDefaultLocale.js.map +1 -0
- package/lib/module/trackCommunityAccess.js +33 -0
- package/lib/module/trackCommunityAccess.js.map +1 -0
- package/lib/module/trackCustomEvent.js +36 -0
- package/lib/module/trackCustomEvent.js.map +1 -0
- package/lib/module/types/sdkEvents.js +2 -0
- package/lib/module/types/sdkEvents.js.map +1 -0
- package/lib/module/types/urlOpeningStrategy.js +23 -0
- package/lib/module/types/urlOpeningStrategy.js.map +1 -0
- package/lib/module/updateNotSeenNotificationsCount.js +33 -0
- package/lib/module/updateNotSeenNotificationsCount.js.map +1 -0
- package/lib/typescript/src/OctopusUIView.d.ts +32 -0
- package/lib/typescript/src/OctopusUIView.d.ts.map +1 -0
- package/lib/typescript/src/addHasAccessToCommunityListener.d.ts +27 -0
- package/lib/typescript/src/addHasAccessToCommunityListener.d.ts.map +1 -0
- package/lib/typescript/src/addNavigateToUrlListener.d.ts +31 -0
- package/lib/typescript/src/addNavigateToUrlListener.d.ts.map +1 -0
- package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts +24 -0
- package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts.map +1 -0
- package/lib/typescript/src/addSDKEventListener.d.ts +43 -0
- package/lib/typescript/src/addSDKEventListener.d.ts.map +1 -0
- package/lib/typescript/src/connectUser.d.ts +24 -8
- package/lib/typescript/src/connectUser.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/initialize.d.ts +22 -12
- package/lib/typescript/src/initialize.d.ts.map +1 -1
- package/lib/typescript/src/openUI.d.ts +28 -1
- package/lib/typescript/src/openUI.d.ts.map +1 -1
- package/lib/typescript/src/overrideCommunityAccess.d.ts +30 -0
- package/lib/typescript/src/overrideCommunityAccess.d.ts.map +1 -0
- package/lib/typescript/src/overrideDefaultLocale.d.ts +37 -0
- package/lib/typescript/src/overrideDefaultLocale.d.ts.map +1 -0
- package/lib/typescript/src/trackCommunityAccess.d.ts +27 -0
- package/lib/typescript/src/trackCommunityAccess.d.ts.map +1 -0
- package/lib/typescript/src/trackCustomEvent.d.ts +30 -0
- package/lib/typescript/src/trackCustomEvent.d.ts.map +1 -0
- package/lib/typescript/src/types/sdkEvents.d.ts +222 -0
- package/lib/typescript/src/types/sdkEvents.d.ts.map +1 -0
- package/lib/typescript/src/types/urlOpeningStrategy.d.ts +20 -0
- package/lib/typescript/src/types/urlOpeningStrategy.d.ts.map +1 -0
- package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts +27 -0
- package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/OctopusUIView.tsx +57 -0
- package/src/addHasAccessToCommunityListener.ts +38 -0
- package/src/addNavigateToUrlListener.ts +54 -0
- package/src/addNotSeenNotificationsCountListener.ts +35 -0
- package/src/addSDKEventListener.ts +49 -0
- package/src/connectUser.ts +24 -8
- package/src/index.ts +13 -0
- package/src/initialize.ts +23 -12
- package/src/openUI.ts +32 -2
- package/src/overrideCommunityAccess.ts +33 -0
- package/src/overrideDefaultLocale.ts +88 -0
- package/src/trackCommunityAccess.ts +30 -0
- package/src/trackCustomEvent.ts +36 -0
- package/src/types/sdkEvents.ts +315 -0
- package/src/types/urlOpeningStrategy.ts +20 -0
- package/src/updateNotSeenNotificationsCount.ts +30 -0
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +0 -155
- package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +0 -422
package/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# @octopus-community/react-native
|
|
2
2
|
|
|
3
|
-
React Native module for the Octopus Community [Android](https://github.com/Octopus-Community/octopus-sdk-android) and [Swift](https://github.com/Octopus-Community/octopus-sdk-swift) SDKs
|
|
3
|
+
React Native module for the Octopus Community [Android](https://github.com/Octopus-Community/octopus-sdk-android) and [Swift](https://github.com/Octopus-Community/octopus-sdk-swift) SDKs.
|
|
4
4
|
|
|
5
|
+
**Official documentation:** [Octopus Developer Guide](https://doc.octopuscommunity.com/) — for concepts, SSO setup, and native SDK details.
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
5
8
|
- [Installation](#installation)
|
|
6
9
|
- [iOS setup](#ios-setup)
|
|
7
10
|
- [Compatibility table](#compatibility-table)
|
|
@@ -11,6 +14,18 @@ React Native module for the Octopus Community [Android](https://github.com/Octop
|
|
|
11
14
|
- [API docs](./docs/api/README.md)
|
|
12
15
|
- [Example app](./example)
|
|
13
16
|
- [Troubleshooting](#troubleshooting)
|
|
17
|
+
- [Documentation index](./docs/README.md)
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **SSO (Single Sign-On)** — Connect your users with JWT from your backend; app-managed profile fields
|
|
22
|
+
- **Theme customization** — Colors, fonts, logo; light/dark and dual-mode support
|
|
23
|
+
- **Display modes** — Fullscreen via [`openUI()`](./docs/api/functions/openUI.md) or embedded via the `OctopusUIView` component (see [API reference](./docs/api/README.md))
|
|
24
|
+
- **Reactive events** — Unread notification count, community access state, and typed SDK events (content, interactions, gamification, navigation)
|
|
25
|
+
- **Community access / A/B testing** — Two cases: (1) **Octopus manages the cohort**: the SDK decides who has access; use [`overrideCommunityAccess`](./docs/api/functions/overrideCommunityAccess.md) to override for testing and [`addHasAccessToCommunityListener`](./docs/api/functions/addHasAccessToCommunityListener.md) to react to the state. (2) **Your app manages access**: your app decides who sees the community (e.g. your own feature flag); use [`trackCommunityAccess`](./docs/api/functions/trackCommunityAccess.md) to report the value to Octopus for analytics only (it does not change actual access).
|
|
26
|
+
- **Locale override** — Set SDK UI language programmatically ([`overrideDefaultLocale`](./docs/api/functions/overrideDefaultLocale.md))
|
|
27
|
+
- **Custom analytics** — Track custom events ([`trackCustomEvent`](./docs/api/functions/trackCustomEvent.md))
|
|
28
|
+
- **URL interception** — Handle link taps in your app ([`addNavigateToUrlListener`](./docs/api/functions/addNavigateToUrlListener.md))
|
|
14
29
|
|
|
15
30
|
## Installation
|
|
16
31
|
|
|
@@ -35,17 +50,15 @@ Due to a bug in XCode 15, you might need to set `ENABLE_USER_SCRIPT_SANDBOXING`
|
|
|
35
50
|
|
|
36
51
|
### Compatibility table
|
|
37
52
|
|
|
38
|
-
| React Native version(s) | Android | iOS | Old arch | New arch |
|
|
39
|
-
|-------------------------| ------- |-------| -------- | ------------- |
|
|
40
|
-
| v0.
|
|
53
|
+
| React Native version(s) | Android | iOS | Old arch | New arch | Octopus native SDK |
|
|
54
|
+
|-------------------------| ------- |-------| -------- | ------------- | ------------------- |
|
|
55
|
+
| v0.81.x (tested 0.81.4) | 5.0+ | 14.0+ | ✅ | Interop layer | 1.9 |
|
|
41
56
|
|
|
42
|
-
* Older React Native versions
|
|
43
|
-
* New
|
|
44
|
-
* Other requirements
|
|
45
|
-
* Android
|
|
46
|
-
|
|
47
|
-
* iOS
|
|
48
|
-
* XCode 16.0+
|
|
57
|
+
* Older React Native versions (e.g. v0.78+) may work but are untested
|
|
58
|
+
* New architecture is supported via the React Native interoperability layer
|
|
59
|
+
* Other requirements:
|
|
60
|
+
* **Android**: Kotlin 2.x
|
|
61
|
+
* **iOS**: Xcode 16.0+
|
|
49
62
|
|
|
50
63
|
### Expo
|
|
51
64
|
|
|
@@ -68,9 +81,9 @@ Configure `use_frameworks` (static or dynamic) with `expo-build-properties`:
|
|
|
68
81
|
|
|
69
82
|
Initialize the SDK with your API key with [`initialize`](./docs/api/functions/initialize.md).
|
|
70
83
|
|
|
71
|
-
Choose whether your app or Octopus handles user
|
|
84
|
+
Choose whether your app or Octopus handles user authentication:
|
|
72
85
|
|
|
73
|
-
#### Octopus-handled
|
|
86
|
+
#### Octopus-handled authentication
|
|
74
87
|
|
|
75
88
|
```ts
|
|
76
89
|
import { initialize } from '@octopus-community/react-native';
|
|
@@ -85,9 +98,9 @@ await initialize({
|
|
|
85
98
|
|
|
86
99
|
For SSO mode:
|
|
87
100
|
|
|
88
|
-
* your app manages user authentication and certain profile fields
|
|
89
|
-
* you
|
|
90
|
-
|
|
101
|
+
* your app manages user authentication and certain profile fields; you specify which profile fields your app will handle directly
|
|
102
|
+
* you must provide a token provider that returns a valid JWT for the connected user. **The JWT must be signed on your backend** using the secret key provided by Octopus — never embed the secret in the app. See [Generate a signed JWT for SSO](https://doc.octopuscommunity.com/backend/sso)
|
|
103
|
+
* in React components, use the `useUserTokenProvider` hook to supply the token:
|
|
91
104
|
|
|
92
105
|
```ts
|
|
93
106
|
import {
|
|
@@ -148,7 +161,6 @@ await connectUser({
|
|
|
148
161
|
username: 'john_doe',
|
|
149
162
|
profilePicture: 'https://example.com/avatar.jpg',
|
|
150
163
|
biography: 'Software developer',
|
|
151
|
-
legalAgeReached: true,
|
|
152
164
|
},
|
|
153
165
|
});
|
|
154
166
|
|
|
@@ -162,9 +174,10 @@ editUserSubscription.remove();
|
|
|
162
174
|
|
|
163
175
|
### Show the UI
|
|
164
176
|
|
|
165
|
-
|
|
177
|
+
You can show the Octopus Community in two ways:
|
|
166
178
|
|
|
167
|
-
|
|
179
|
+
- **Fullscreen** — Call [`openUI()`](./docs/api/functions/openUI.md) to open the native Octopus home screen (modal-style). Use [`closeUI()`](./docs/api/functions/closeUI.md) to dismiss it.
|
|
180
|
+
- **Embedded** — Render the `OctopusUIView` component inside your React tree for an in-screen embed. Pass `displayMode="embed"` or `displayMode="fullscreen"` and optional theme/options. See the [API reference](./docs/api/README.md) and the [example app](./example) for usage.
|
|
168
181
|
|
|
169
182
|
### Theme Customization
|
|
170
183
|
|
|
@@ -438,24 +451,16 @@ For details about the Typescript API, head to the [API docs](./docs/api/README.m
|
|
|
438
451
|
|
|
439
452
|
### Example app
|
|
440
453
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
- **Complete theming system** with multiple color themes and font customizations
|
|
444
|
-
- **Dark/Light mode management** with system detection and forced modes
|
|
445
|
-
- **Dual-mode color themes** with separate light and dark color sets
|
|
446
|
-
- **Font customization** with different font types (serif, monospace, default) and sizes
|
|
447
|
-
- **Theme switching** by re-initializing the SDK with new configurations
|
|
448
|
-
- **Logo customization** with bundled image assets
|
|
449
|
-
- **Interactive theme controls** to test different configurations
|
|
454
|
+
The [example app](./example) demonstrates the full SDK surface. From the root, run `yarn example start` then `yarn example ios` or `yarn example android`. It uses a tabbed UI:
|
|
450
455
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
456
|
+
| Tab | Purpose |
|
|
457
|
+
|-----|---------|
|
|
458
|
+
| **Setup** | Initialize the SDK (API key, SSO/Octopus mode), connect/disconnect user, display mode (fullscreen vs embed), locale override, URL interception toggle |
|
|
459
|
+
| **Community** | Open the Octopus UI (fullscreen or embedded) with the current theme and options |
|
|
460
|
+
| **Theme** | Theming: system/light/dark, color set, fonts, logo, bottom inset; see changes when reopening the UI |
|
|
461
|
+
| **SDK Data** | Notifications count (refresh, listener), community access (override, track, listener), custom events, SDK event log |
|
|
457
462
|
|
|
458
|
-
|
|
463
|
+
See [example/README.md](./example/README.md) for environment variables (API key, SSO user/token) and run instructions. The example is the best reference for implementing SSO, theming, reactive events, and URL interception in your app.
|
|
459
464
|
|
|
460
465
|
## Troubleshooting
|
|
461
466
|
|
package/android/build.gradle
CHANGED
|
@@ -85,6 +85,8 @@ dependencies {
|
|
|
85
85
|
implementation "com.facebook.react:react-android"
|
|
86
86
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
87
87
|
implementation platform("androidx.compose:compose-bom:$compose_bom_version")
|
|
88
|
+
implementation "androidx.compose.foundation:foundation"
|
|
89
|
+
implementation "androidx.compose.material3:material3"
|
|
88
90
|
implementation "androidx.navigation:navigation-compose:$android_x_navigation_version"
|
|
89
91
|
|
|
90
92
|
api "com.octopuscommunity:octopus-sdk:$octopus_community_version"
|
|
@@ -3,7 +3,7 @@ OctopusReactNativeSdk_minSdkVersion=24
|
|
|
3
3
|
OctopusReactNativeSdk_targetSdkVersion=34
|
|
4
4
|
OctopusReactNativeSdk_compileSdkVersion=35
|
|
5
5
|
OctopusReactNativeSdk_ndkVersion=27.1.12297006
|
|
6
|
-
OctopusReactNativeSdk_composeBomVersion=2025.
|
|
6
|
+
OctopusReactNativeSdk_composeBomVersion=2025.10.00
|
|
7
7
|
OctopusReactNativeSdk_androidXNavigationVersion=2.9.0
|
|
8
8
|
|
|
9
|
-
OctopusReactNativeSdk_octopusCommunityVersion=1.
|
|
9
|
+
OctopusReactNativeSdk_octopusCommunityVersion=1.9.1
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
package="com.octopuscommunity.octopusreactnativesdk">
|
|
3
3
|
<application>
|
|
4
4
|
<activity
|
|
5
|
-
android:name=".
|
|
5
|
+
android:name=".OctopusActivity"
|
|
6
|
+
android:windowSoftInputMode="adjustResize"
|
|
6
7
|
android:exported="false"
|
|
7
8
|
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"/>
|
|
8
9
|
</application>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
<application>
|
|
3
3
|
<activity
|
|
4
|
-
android:name=".
|
|
4
|
+
android:name=".OctopusActivity"
|
|
5
5
|
android:exported="false"
|
|
6
|
+
android:windowSoftInputMode="adjustResize"
|
|
6
7
|
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"/>
|
|
7
8
|
</application>
|
|
8
9
|
</manifest>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package com.octopuscommunity.octopusreactnativesdk
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.IntentFilter
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import androidx.activity.ComponentActivity
|
|
10
|
+
import androidx.activity.compose.setContent
|
|
11
|
+
import androidx.activity.enableEdgeToEdge
|
|
12
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
13
|
+
import androidx.compose.ui.Modifier
|
|
14
|
+
|
|
15
|
+
class OctopusActivity : ComponentActivity() {
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
const val EXTRA_INTERCEPT_URLS = "interceptUrls"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private val closeUIReceiver = object : BroadcastReceiver() {
|
|
22
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
23
|
+
finish()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
28
|
+
super.onCreate(savedInstanceState)
|
|
29
|
+
enableEdgeToEdge()
|
|
30
|
+
registerCloseUIReceiver()
|
|
31
|
+
val interceptUrls = intent.getBooleanExtra(EXTRA_INTERCEPT_URLS, false)
|
|
32
|
+
|
|
33
|
+
setContent {
|
|
34
|
+
OctopusContent(
|
|
35
|
+
backButton = true,
|
|
36
|
+
interceptUrls = interceptUrls,
|
|
37
|
+
onBack = { finish() }
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun onDestroy() {
|
|
43
|
+
super.onDestroy()
|
|
44
|
+
unregisterReceiver(closeUIReceiver)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private fun registerCloseUIReceiver() {
|
|
48
|
+
val intentFilter = IntentFilter(OctopusUIController.CLOSE_UI_ACTION)
|
|
49
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
50
|
+
registerReceiver(closeUIReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
|
|
51
|
+
} else {
|
|
52
|
+
@Suppress("UnspecifiedRegisterReceiverFlag")
|
|
53
|
+
registerReceiver(closeUIReceiver, intentFilter)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
package com.octopuscommunity.octopusreactnativesdk
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.compose.runtime.Composable
|
|
8
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
9
|
+
import androidx.compose.runtime.getValue
|
|
10
|
+
import androidx.compose.runtime.mutableStateOf
|
|
11
|
+
import androidx.compose.runtime.remember
|
|
12
|
+
import androidx.compose.runtime.setValue
|
|
13
|
+
import androidx.compose.ui.Modifier
|
|
14
|
+
import androidx.compose.ui.graphics.Color
|
|
15
|
+
import androidx.compose.ui.graphics.asImageBitmap
|
|
16
|
+
import androidx.compose.ui.graphics.painter.BitmapPainter
|
|
17
|
+
import androidx.compose.ui.graphics.painter.Painter
|
|
18
|
+
import androidx.compose.ui.platform.LocalContext
|
|
19
|
+
import androidx.compose.ui.text.TextStyle
|
|
20
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
21
|
+
import androidx.compose.ui.unit.dp
|
|
22
|
+
import androidx.compose.ui.unit.sp
|
|
23
|
+
import androidx.core.graphics.toColorInt
|
|
24
|
+
import androidx.navigation.compose.NavHost
|
|
25
|
+
import androidx.navigation.compose.composable
|
|
26
|
+
import androidx.navigation.compose.rememberNavController
|
|
27
|
+
import com.octopuscommunity.sdk.ui.OctopusImagesDefaults
|
|
28
|
+
import com.octopuscommunity.sdk.ui.OctopusTheme
|
|
29
|
+
import com.octopuscommunity.sdk.ui.OctopusTypography
|
|
30
|
+
import com.octopuscommunity.sdk.ui.OctopusTypographyDefaults
|
|
31
|
+
import com.octopuscommunity.sdk.ui.components.UrlOpeningStrategy
|
|
32
|
+
import com.octopuscommunity.sdk.ui.home.OctopusHomeDefaults
|
|
33
|
+
import com.octopuscommunity.sdk.ui.home.OctopusHomeScreen
|
|
34
|
+
import com.octopuscommunity.sdk.ui.octopusComposables
|
|
35
|
+
import com.octopuscommunity.sdk.ui.octopusDarkColorScheme
|
|
36
|
+
import com.octopuscommunity.sdk.ui.octopusLightColorScheme
|
|
37
|
+
import kotlinx.coroutines.Dispatchers
|
|
38
|
+
import kotlinx.coroutines.withContext
|
|
39
|
+
import java.net.URL
|
|
40
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
41
|
+
|
|
42
|
+
// Cache for loaded images to avoid reloading on recomposition
|
|
43
|
+
private val imageCache = ConcurrentHashMap<String, Painter?>()
|
|
44
|
+
|
|
45
|
+
// Cache for drawable resources list to avoid repeated reflection
|
|
46
|
+
private var cachedDrawableResources: List<String>? = null
|
|
47
|
+
|
|
48
|
+
/** Shared composable for both fullscreen Activity and embedded ViewManager. */
|
|
49
|
+
@Composable
|
|
50
|
+
internal fun OctopusContent(
|
|
51
|
+
modifier: Modifier = Modifier,
|
|
52
|
+
backButton: Boolean = false,
|
|
53
|
+
interceptUrls: Boolean = false,
|
|
54
|
+
onBack: () -> Unit = {}
|
|
55
|
+
) {
|
|
56
|
+
val context = LocalContext.current
|
|
57
|
+
|
|
58
|
+
// Get theme config once when UI is created - no need for polling
|
|
59
|
+
val themeConfig = OctopusThemeManager.getThemeConfig()
|
|
60
|
+
|
|
61
|
+
// Function to detect if system is in dark mode
|
|
62
|
+
fun isSystemInDarkTheme(): Boolean {
|
|
63
|
+
return (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle logo loading using built-in Android capabilities
|
|
67
|
+
var logoPainter by remember { mutableStateOf<Painter?>(null) }
|
|
68
|
+
|
|
69
|
+
LaunchedEffect(themeConfig) {
|
|
70
|
+
themeConfig?.logoSource?.let { logoSource ->
|
|
71
|
+
val uri = logoSource.getString("uri")
|
|
72
|
+
if (uri != null) {
|
|
73
|
+
logoPainter = loadImageFromUri(uri, context)
|
|
74
|
+
} else {
|
|
75
|
+
logoPainter = null
|
|
76
|
+
}
|
|
77
|
+
} ?: run {
|
|
78
|
+
// No theme config or no logo source
|
|
79
|
+
logoPainter = null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create images based on theme config
|
|
84
|
+
val images = if (logoPainter != null) {
|
|
85
|
+
OctopusImagesDefaults.images(logo = logoPainter)
|
|
86
|
+
} else {
|
|
87
|
+
OctopusImagesDefaults.images()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create typography based on theme config
|
|
91
|
+
val typography = createCustomTypography(themeConfig)
|
|
92
|
+
val uiConfiguration = OctopusUIConfigurationManager.getUIConfiguration()
|
|
93
|
+
|
|
94
|
+
val bottomContentPadding = uiConfiguration?.bottomContentPadding?.toFloat()?.dp
|
|
95
|
+
|
|
96
|
+
// Apply theme with custom colors, logo, typography, and content padding
|
|
97
|
+
OctopusTheme(
|
|
98
|
+
colorScheme = when (themeConfig?.colorScheme) {
|
|
99
|
+
"dark" -> octopusDarkColorScheme()
|
|
100
|
+
"light" -> octopusLightColorScheme()
|
|
101
|
+
else -> if (isSystemInDarkTheme()) octopusDarkColorScheme() else octopusLightColorScheme()
|
|
102
|
+
}.let { colorScheme ->
|
|
103
|
+
// Apply custom colors if theme config is provided
|
|
104
|
+
colorScheme.copy(
|
|
105
|
+
primary = themeConfig?.primaryColor?.let {
|
|
106
|
+
Color(it.toColorInt())
|
|
107
|
+
} ?: colorScheme.primary,
|
|
108
|
+
primaryLow = themeConfig?.primaryLowContrastColor?.let {
|
|
109
|
+
Color(it.toColorInt())
|
|
110
|
+
} ?: colorScheme.primaryLow,
|
|
111
|
+
primaryHigh = themeConfig?.primaryHighContrastColor?.let {
|
|
112
|
+
Color(it.toColorInt())
|
|
113
|
+
} ?: colorScheme.primaryHigh,
|
|
114
|
+
onPrimary = themeConfig?.onPrimaryColor?.let {
|
|
115
|
+
Color(it.toColorInt())
|
|
116
|
+
} ?: colorScheme.onPrimary
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
images = images,
|
|
120
|
+
typography = typography
|
|
121
|
+
) {
|
|
122
|
+
val navController = rememberNavController()
|
|
123
|
+
|
|
124
|
+
NavHost(
|
|
125
|
+
modifier = modifier,
|
|
126
|
+
navController = navController,
|
|
127
|
+
startDestination = "OctopusHome"
|
|
128
|
+
) {
|
|
129
|
+
composable(route = "OctopusHome") {
|
|
130
|
+
OctopusHomeScreen(
|
|
131
|
+
navController = navController,
|
|
132
|
+
contentPadding = if (bottomContentPadding != null) {
|
|
133
|
+
OctopusHomeDefaults.contentPadding(bottom = bottomContentPadding)
|
|
134
|
+
} else {
|
|
135
|
+
OctopusHomeDefaults.contentPadding()
|
|
136
|
+
},
|
|
137
|
+
backIcon = backButton,
|
|
138
|
+
onBack = onBack,
|
|
139
|
+
onNavigateToLogin = {
|
|
140
|
+
OctopusEventEmitter.instance?.emitLoginRequired()
|
|
141
|
+
},
|
|
142
|
+
onNavigateToProfileEdit = { fieldToEdit ->
|
|
143
|
+
OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
|
|
144
|
+
},
|
|
145
|
+
onNavigateToUrl = { url: String ->
|
|
146
|
+
if (interceptUrls) {
|
|
147
|
+
OctopusEventEmitter.instance?.emitNavigateToUrl(url)
|
|
148
|
+
UrlOpeningStrategy.HandledByApp
|
|
149
|
+
} else {
|
|
150
|
+
UrlOpeningStrategy.HandledByOctopus
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
octopusComposables(
|
|
156
|
+
navController = navController,
|
|
157
|
+
container = { _, content ->
|
|
158
|
+
OctopusTheme(content = content)
|
|
159
|
+
},
|
|
160
|
+
onBack = onBack,
|
|
161
|
+
onNavigateToLogin = {
|
|
162
|
+
OctopusEventEmitter.instance?.emitLoginRequired()
|
|
163
|
+
},
|
|
164
|
+
onNavigateToProfileEdit = { fieldToEdit ->
|
|
165
|
+
OctopusEventEmitter.instance?.emitEditUser(fieldToEdit)
|
|
166
|
+
},
|
|
167
|
+
onNavigateToUrl = { url: String ->
|
|
168
|
+
if (interceptUrls) {
|
|
169
|
+
OctopusEventEmitter.instance?.emitNavigateToUrl(url)
|
|
170
|
+
UrlOpeningStrategy.HandledByApp
|
|
171
|
+
} else {
|
|
172
|
+
UrlOpeningStrategy.HandledByOctopus
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun createCustomTypography(themeConfig: OctopusThemeConfig?): OctopusTypography {
|
|
181
|
+
val defaultTypography = OctopusTypographyDefaults.typography()
|
|
182
|
+
|
|
183
|
+
if (themeConfig?.fonts == null) {
|
|
184
|
+
return defaultTypography
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
val fontsConfig = themeConfig.fonts!!
|
|
188
|
+
val textStyles = fontsConfig.textStyles
|
|
189
|
+
|
|
190
|
+
// Create custom typography based on new unified font configuration
|
|
191
|
+
if (textStyles != null && textStyles.isNotEmpty()) {
|
|
192
|
+
return OctopusTypographyDefaults.typography(
|
|
193
|
+
title1 = createTextStyle(textStyles["title1"], defaultTypography.title1),
|
|
194
|
+
title2 = createTextStyle(textStyles["title2"], defaultTypography.title2),
|
|
195
|
+
body1 = createTextStyle(textStyles["body1"], defaultTypography.body1),
|
|
196
|
+
body2 = createTextStyle(textStyles["body2"], defaultTypography.body2),
|
|
197
|
+
caption1 = createTextStyle(textStyles["caption1"], defaultTypography.caption1),
|
|
198
|
+
caption2 = createTextStyle(textStyles["caption2"], defaultTypography.caption2)
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return defaultTypography
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private fun createTextStyle(
|
|
206
|
+
textStyleConfig: OctopusTextStyleConfig?,
|
|
207
|
+
defaultStyle: TextStyle
|
|
208
|
+
): TextStyle {
|
|
209
|
+
if (textStyleConfig == null) {
|
|
210
|
+
return defaultStyle
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
val fontFamily = when (textStyleConfig.fontType) {
|
|
214
|
+
"serif" -> FontFamily.Serif
|
|
215
|
+
"monospace" -> FontFamily.Monospace
|
|
216
|
+
"default" -> FontFamily.Default
|
|
217
|
+
else -> FontFamily.Default
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
val fontSize = textStyleConfig.fontSize?.let {
|
|
221
|
+
// Use points directly as sp (1 point ≈ 1 sp on Android)
|
|
222
|
+
it.sp
|
|
223
|
+
} ?: defaultStyle.fontSize
|
|
224
|
+
|
|
225
|
+
return defaultStyle.copy(
|
|
226
|
+
fontFamily = fontFamily,
|
|
227
|
+
fontSize = fontSize
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private suspend fun loadImageFromUri(uri: String, context: Context): Painter? {
|
|
232
|
+
// Check cache first
|
|
233
|
+
imageCache[uri]?.let { return it }
|
|
234
|
+
|
|
235
|
+
return withContext(Dispatchers.IO) {
|
|
236
|
+
try {
|
|
237
|
+
val result = when {
|
|
238
|
+
// Network URLs - load directly
|
|
239
|
+
uri.startsWith("http://", ignoreCase = true) ||
|
|
240
|
+
uri.startsWith("https://", ignoreCase = true) -> {
|
|
241
|
+
loadFromNetwork(uri)
|
|
242
|
+
}
|
|
243
|
+
// Everything else - try as React Native asset
|
|
244
|
+
else -> {
|
|
245
|
+
loadReactNativeAsset(uri, context)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Cache result (including null to avoid retrying failed loads)
|
|
250
|
+
imageCache[uri] = result
|
|
251
|
+
result
|
|
252
|
+
} catch (e: Exception) {
|
|
253
|
+
Log.w("OctopusActivity", "Failed to load image from URI: $uri", e)
|
|
254
|
+
imageCache[uri] = null
|
|
255
|
+
null
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun loadFromNetwork(url: String): Painter? {
|
|
261
|
+
return try {
|
|
262
|
+
URL(url).openStream().use { inputStream ->
|
|
263
|
+
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
264
|
+
bitmap?.let { BitmapPainter(it.asImageBitmap()) }
|
|
265
|
+
}
|
|
266
|
+
} catch (e: Exception) {
|
|
267
|
+
Log.w("OctopusActivity", "Failed to load image from network: $url", e)
|
|
268
|
+
null
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private fun loadReactNativeAsset(assetName: String, context: Context): Painter? {
|
|
273
|
+
return try {
|
|
274
|
+
// First try: Direct drawable resource lookup (most common case)
|
|
275
|
+
val drawableResult = loadFromDrawableResources(assetName, context)
|
|
276
|
+
if (drawableResult != null) {
|
|
277
|
+
return drawableResult
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Second try: Assets folder with strategic path checking
|
|
281
|
+
loadFromAssetsFolder(assetName, context)
|
|
282
|
+
} catch (e: Exception) {
|
|
283
|
+
Log.w("OctopusActivity", "Failed to load asset: $assetName", e)
|
|
284
|
+
null
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private fun loadFromAssetsFolder(assetName: String, context: Context): Painter? {
|
|
289
|
+
// Check if assetName already has extension
|
|
290
|
+
val hasExtension = assetName.contains(".")
|
|
291
|
+
val extensions = if (hasExtension) listOf("") else listOf("png", "jpg", "jpeg", "webp")
|
|
292
|
+
|
|
293
|
+
val folders = listOf(
|
|
294
|
+
"",
|
|
295
|
+
"drawable-mdpi",
|
|
296
|
+
"drawable-hdpi",
|
|
297
|
+
"drawable-xhdpi",
|
|
298
|
+
"drawable-xxhdpi",
|
|
299
|
+
"drawable-xxxhdpi",
|
|
300
|
+
"drawable"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
for (folder in folders) {
|
|
304
|
+
for (ext in extensions) {
|
|
305
|
+
val filename = if (ext.isEmpty()) assetName else "$assetName.$ext"
|
|
306
|
+
val path = if (folder.isEmpty()) filename else "$folder/$filename"
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
context.assets.open(path).use { inputStream ->
|
|
310
|
+
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
311
|
+
if (bitmap != null) {
|
|
312
|
+
return BitmapPainter(bitmap.asImageBitmap())
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (e: Exception) {
|
|
316
|
+
// Continue to next path
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private fun loadFromDrawableResources(assetName: String, context: Context): Painter? {
|
|
325
|
+
return try {
|
|
326
|
+
// Try exact match first
|
|
327
|
+
val resourceId = context.resources.getIdentifier(
|
|
328
|
+
assetName, "drawable", context.packageName
|
|
329
|
+
)
|
|
330
|
+
if (resourceId != 0) {
|
|
331
|
+
return loadBitmapFromResource(context, resourceId)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fallback: fuzzy search (only if exact match fails)
|
|
335
|
+
val drawableResources = getAllDrawableResources(context)
|
|
336
|
+
for (resourceName in drawableResources) {
|
|
337
|
+
if (isResourceNameMatch(resourceName, assetName)) {
|
|
338
|
+
val resId = context.resources.getIdentifier(
|
|
339
|
+
resourceName, "drawable", context.packageName
|
|
340
|
+
)
|
|
341
|
+
if (resId != 0) {
|
|
342
|
+
return loadBitmapFromResource(context, resId)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
null
|
|
348
|
+
} catch (e: Exception) {
|
|
349
|
+
null
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private fun loadBitmapFromResource(context: Context, resourceId: Int): Painter? {
|
|
354
|
+
return try {
|
|
355
|
+
val bitmap = BitmapFactory.decodeResource(context.resources, resourceId)
|
|
356
|
+
bitmap?.let { BitmapPainter(it.asImageBitmap()) }
|
|
357
|
+
} catch (e: Exception) {
|
|
358
|
+
null
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private fun getAllDrawableResources(context: Context): List<String> {
|
|
363
|
+
// Return cached result if available
|
|
364
|
+
cachedDrawableResources?.let { return it }
|
|
365
|
+
|
|
366
|
+
return try {
|
|
367
|
+
val packageName = context.packageName
|
|
368
|
+
val drawableResources = mutableListOf<String>()
|
|
369
|
+
|
|
370
|
+
// Get all drawable resources by scanning the R.drawable class
|
|
371
|
+
val drawableClass = Class.forName("$packageName.R\$drawable")
|
|
372
|
+
val fields = drawableClass.declaredFields
|
|
373
|
+
|
|
374
|
+
for (field in fields) {
|
|
375
|
+
if (field.type == Int::class.javaPrimitiveType) {
|
|
376
|
+
drawableResources.add(field.name)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Cache for future use
|
|
381
|
+
cachedDrawableResources = drawableResources
|
|
382
|
+
drawableResources
|
|
383
|
+
} catch (e: Exception) {
|
|
384
|
+
Log.w("OctopusActivity", "Failed to get drawable resources", e)
|
|
385
|
+
emptyList()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private fun isResourceNameMatch(resourceName: String, assetName: String): Boolean {
|
|
390
|
+
val normalizedResourceName = resourceName.lowercase()
|
|
391
|
+
val normalizedAssetName = assetName.lowercase()
|
|
392
|
+
|
|
393
|
+
// Only check if one contains the other - removed dangerous generic patterns
|
|
394
|
+
return normalizedResourceName.contains(normalizedAssetName) ||
|
|
395
|
+
normalizedAssetName.contains(normalizedResourceName)
|
|
396
|
+
}
|
package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventEmitter.kt
CHANGED
|
@@ -34,6 +34,28 @@ class OctopusEventEmitter(private val reactContext: ReactContext) {
|
|
|
34
34
|
sendEvent("userTokenRequest", params)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
fun emitNotSeenNotificationsCountChanged(count: Int) {
|
|
38
|
+
val params = Arguments.createMap()
|
|
39
|
+
params.putInt("count", count)
|
|
40
|
+
sendEvent("notSeenNotificationsCountChanged", params)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun emitHasAccessToCommunityChanged(hasAccess: Boolean) {
|
|
44
|
+
val params = Arguments.createMap()
|
|
45
|
+
params.putBoolean("hasAccess", hasAccess)
|
|
46
|
+
sendEvent("hasAccessToCommunityChanged", params)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun emitSDKEvent(eventData: WritableMap) {
|
|
50
|
+
sendEvent("sdkEvent", eventData)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun emitNavigateToUrl(url: String) {
|
|
54
|
+
val params = Arguments.createMap()
|
|
55
|
+
params.putString("url", url)
|
|
56
|
+
sendEvent("navigateToUrl", params)
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
private fun sendEvent(eventName: String, params: WritableMap?) {
|
|
38
60
|
if (listenerCount > 0) {
|
|
39
61
|
reactContext
|