@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.
Files changed (112) hide show
  1. package/OctopusReactNativeSdk.podspec +1 -1
  2. package/README.md +40 -35
  3. package/android/build.gradle +2 -0
  4. package/android/gradle.properties +2 -2
  5. package/android/src/main/AndroidManifest.xml +2 -1
  6. package/android/src/main/AndroidManifestNew.xml +2 -1
  7. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusActivity.kt +56 -0
  8. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusContent.kt +396 -0
  9. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventEmitter.kt +22 -0
  10. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusEventSerializer.kt +339 -0
  11. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactModule.kt +326 -0
  12. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/{OctopusReactNativeSdkPackage.kt → OctopusReactPackage.kt} +3 -3
  13. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSDKInitializer.kt +53 -9
  14. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusSSOAuthenticator.kt +5 -15
  15. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfiguration.kt +6 -0
  16. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIConfigurationManager.kt +12 -0
  17. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIController.kt +17 -2
  18. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIViewManager.kt +63 -0
  19. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/ProfileFieldMapper.kt +2 -2
  20. package/ios/OctopusEventManager.swift +27 -0
  21. package/ios/OctopusEventSerializer.swift +271 -0
  22. package/ios/OctopusReactNativeSdk.mm +26 -1
  23. package/ios/OctopusReactNativeSdk.swift +225 -3
  24. package/ios/OctopusSDKInitializer.swift +32 -0
  25. package/ios/OctopusSSOAuthenticator.swift +1 -5
  26. package/ios/OctopusUIConfiguration.swift +6 -0
  27. package/ios/OctopusUIManager.swift +134 -10
  28. package/ios/OctopusUIViewManager.m +7 -0
  29. package/ios/OctopusUIViewManager.swift +37 -0
  30. package/lib/module/OctopusUIView.js +39 -0
  31. package/lib/module/OctopusUIView.js.map +1 -0
  32. package/lib/module/addHasAccessToCommunityListener.js +33 -0
  33. package/lib/module/addHasAccessToCommunityListener.js.map +1 -0
  34. package/lib/module/addNavigateToUrlListener.js +41 -0
  35. package/lib/module/addNavigateToUrlListener.js.map +1 -0
  36. package/lib/module/addNotSeenNotificationsCountListener.js +30 -0
  37. package/lib/module/addNotSeenNotificationsCountListener.js.map +1 -0
  38. package/lib/module/addSDKEventListener.js +48 -0
  39. package/lib/module/addSDKEventListener.js.map +1 -0
  40. package/lib/module/connectUser.js +24 -3
  41. package/lib/module/connectUser.js.map +1 -1
  42. package/lib/module/index.js +12 -0
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/initialize.js +13 -12
  45. package/lib/module/initialize.js.map +1 -1
  46. package/lib/module/openUI.js +23 -2
  47. package/lib/module/openUI.js.map +1 -1
  48. package/lib/module/overrideCommunityAccess.js +36 -0
  49. package/lib/module/overrideCommunityAccess.js.map +1 -0
  50. package/lib/module/overrideDefaultLocale.js +75 -0
  51. package/lib/module/overrideDefaultLocale.js.map +1 -0
  52. package/lib/module/trackCommunityAccess.js +33 -0
  53. package/lib/module/trackCommunityAccess.js.map +1 -0
  54. package/lib/module/trackCustomEvent.js +36 -0
  55. package/lib/module/trackCustomEvent.js.map +1 -0
  56. package/lib/module/types/sdkEvents.js +2 -0
  57. package/lib/module/types/sdkEvents.js.map +1 -0
  58. package/lib/module/types/urlOpeningStrategy.js +23 -0
  59. package/lib/module/types/urlOpeningStrategy.js.map +1 -0
  60. package/lib/module/updateNotSeenNotificationsCount.js +33 -0
  61. package/lib/module/updateNotSeenNotificationsCount.js.map +1 -0
  62. package/lib/typescript/src/OctopusUIView.d.ts +32 -0
  63. package/lib/typescript/src/OctopusUIView.d.ts.map +1 -0
  64. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts +27 -0
  65. package/lib/typescript/src/addHasAccessToCommunityListener.d.ts.map +1 -0
  66. package/lib/typescript/src/addNavigateToUrlListener.d.ts +31 -0
  67. package/lib/typescript/src/addNavigateToUrlListener.d.ts.map +1 -0
  68. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts +24 -0
  69. package/lib/typescript/src/addNotSeenNotificationsCountListener.d.ts.map +1 -0
  70. package/lib/typescript/src/addSDKEventListener.d.ts +43 -0
  71. package/lib/typescript/src/addSDKEventListener.d.ts.map +1 -0
  72. package/lib/typescript/src/connectUser.d.ts +24 -8
  73. package/lib/typescript/src/connectUser.d.ts.map +1 -1
  74. package/lib/typescript/src/index.d.ts +13 -0
  75. package/lib/typescript/src/index.d.ts.map +1 -1
  76. package/lib/typescript/src/initialize.d.ts +22 -12
  77. package/lib/typescript/src/initialize.d.ts.map +1 -1
  78. package/lib/typescript/src/openUI.d.ts +28 -1
  79. package/lib/typescript/src/openUI.d.ts.map +1 -1
  80. package/lib/typescript/src/overrideCommunityAccess.d.ts +30 -0
  81. package/lib/typescript/src/overrideCommunityAccess.d.ts.map +1 -0
  82. package/lib/typescript/src/overrideDefaultLocale.d.ts +37 -0
  83. package/lib/typescript/src/overrideDefaultLocale.d.ts.map +1 -0
  84. package/lib/typescript/src/trackCommunityAccess.d.ts +27 -0
  85. package/lib/typescript/src/trackCommunityAccess.d.ts.map +1 -0
  86. package/lib/typescript/src/trackCustomEvent.d.ts +30 -0
  87. package/lib/typescript/src/trackCustomEvent.d.ts.map +1 -0
  88. package/lib/typescript/src/types/sdkEvents.d.ts +222 -0
  89. package/lib/typescript/src/types/sdkEvents.d.ts.map +1 -0
  90. package/lib/typescript/src/types/urlOpeningStrategy.d.ts +20 -0
  91. package/lib/typescript/src/types/urlOpeningStrategy.d.ts.map +1 -0
  92. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts +27 -0
  93. package/lib/typescript/src/updateNotSeenNotificationsCount.d.ts.map +1 -0
  94. package/package.json +2 -1
  95. package/src/OctopusUIView.tsx +57 -0
  96. package/src/addHasAccessToCommunityListener.ts +38 -0
  97. package/src/addNavigateToUrlListener.ts +54 -0
  98. package/src/addNotSeenNotificationsCountListener.ts +35 -0
  99. package/src/addSDKEventListener.ts +49 -0
  100. package/src/connectUser.ts +24 -8
  101. package/src/index.ts +13 -0
  102. package/src/initialize.ts +23 -12
  103. package/src/openUI.ts +32 -2
  104. package/src/overrideCommunityAccess.ts +33 -0
  105. package/src/overrideDefaultLocale.ts +88 -0
  106. package/src/trackCommunityAccess.ts +30 -0
  107. package/src/trackCustomEvent.ts +36 -0
  108. package/src/types/sdkEvents.ts +315 -0
  109. package/src/types/urlOpeningStrategy.ts +20 -0
  110. package/src/updateNotSeenNotificationsCount.ts +30 -0
  111. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusReactNativeSdkModule.kt +0 -155
  112. package/android/src/main/java/com/octopuscommunity/octopusreactnativesdk/OctopusUIActivity.kt +0 -422
@@ -1,7 +1,7 @@
1
1
  require "json"
2
2
 
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
- octopus_version = '~> 1.5.1'
4
+ octopus_version = '1.9.3'
5
5
 
6
6
  Pod::Spec.new do |s|
7
7
  s.name = "OctopusReactNativeSdk"
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 | Supporting version(s) |
39
- |-------------------------| ------- |-------| -------- | ------------- | --------------------- |
40
- | v0.78+ | 5.0+ | 14.0+ | ✅ | Interop layer | v1+ |
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 might be supported but were untested
43
- * New arch is currently supported with the interoperability layer
44
- * Other requirements for current version:
45
- * Android
46
- * Kotlin version 2
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 authentification:
84
+ Choose whether your app or Octopus handles user authentication:
72
85
 
73
- #### Octopus-handled authentification
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. You specify which profile fields your app will handle directly
89
- * you need to provide a token provider function that returns a valid JWT token for the authenticated user.
90
- * use the `useUserTokenProvider` hook in React components:
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
- Show the Octopus home screen with the [`openUI()`](./docs/api/functions/openUI.md) method.
177
+ You can show the Octopus Community in two ways:
166
178
 
167
- Future versions of this React Native module will let you show the UI in your React components. Please reach out if you need this prioritized.
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
- Take a look at our [example app](./example/src/App.tsx) which demonstrates:
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
- The example app includes a comprehensive theme selector interface that lets you:
452
- - Switch between system, light, and dark modes
453
- - Choose from multiple pre-defined color themes (Theme 1, 2, 3, or None)
454
- - Customize font types (default, serif, monospace) and sizes (small, default, large)
455
- - Toggle custom logo display
456
- - See theme changes applied when the UI reopens
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
- This is the perfect starting point to understand how to implement theming in your own React Native app.
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
 
@@ -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.06.01
6
+ OctopusReactNativeSdk_composeBomVersion=2025.10.00
7
7
  OctopusReactNativeSdk_androidXNavigationVersion=2.9.0
8
8
 
9
- OctopusReactNativeSdk_octopusCommunityVersion=1.5.0
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=".OctopusUIActivity"
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=".OctopusUIActivity"
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
+ }
@@ -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