@dashx/react-native 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.DS_Store CHANGED
Binary file
@@ -8,7 +8,24 @@
8
8
  "Bash(swift build:*)",
9
9
  "Bash(xcodebuild -scheme DashX -destination 'generic/platform=iOS' build)",
10
10
  "Bash(./gradlew :dashx:compileDebugKotlin)",
11
- "Bash(./gradlew :dashx:compileDebugKotlin --info)"
11
+ "Bash(./gradlew :dashx:compileDebugKotlin --info)",
12
+ "Bash(gh pr:*)",
13
+ "WebFetch(domain:github.com)",
14
+ "WebFetch(domain:patch-diff.githubusercontent.com)",
15
+ "WebFetch(domain:raw.githubusercontent.com)",
16
+ "Bash(ls -la /Users/gulshan/Work/dashx-react-native/ios/DashXRCTAppDelegate*)",
17
+ "Bash(npx jest *)",
18
+ "Bash(ls *)",
19
+ "Bash(grep *)",
20
+ "Read(//private/tmp/**)",
21
+ "Bash(javap -p com/dashx/android/DashX.class)",
22
+ "Bash(javap -p 'com/dashx/android/DashX$Companion.class')",
23
+ "Bash(jar xf *)",
24
+ "Bash(javap 'com/dashx/android/DashX$Companion.class')",
25
+ "Bash(javap 'com/dashx/android/data/Asset.class')",
26
+ "Bash(javap 'com/dashx/android/data/AssetData.class')",
27
+ "Read(//Users/gulshan/Work/dashx-ios/**)",
28
+ "WebSearch"
12
29
  ]
13
30
  }
14
31
  }
package/.gitignore CHANGED
@@ -42,5 +42,5 @@ buck-out/
42
42
  \.buckd/
43
43
  *.keystore
44
44
 
45
- # Cursor plans
45
+ # Cursor
46
46
  .cursor
@@ -1,7 +1,6 @@
1
1
  require "json"
2
2
 
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
- folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5
4
 
6
5
  Pod::Spec.new do |s|
7
6
  s.name = "DashXReactNative"
@@ -17,30 +16,10 @@ Pod::Spec.new do |s|
17
16
  s.source_files = "ios/**/*.{h,m,mm,swift}"
18
17
  s.requires_arc = true
19
18
 
20
- # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
21
- # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
22
- if respond_to?(:install_modules_dependencies, true)
23
- install_modules_dependencies(s)
24
- else
25
- s.dependency "React-Core"
19
+ # React Native >= 0.71 provides install_modules_dependencies.
20
+ # Since our minimum supported RN version is 0.74, we can call it directly.
21
+ install_modules_dependencies(s)
26
22
 
27
- # Don't install the dependencies when we run `pod install` in the old architecture.
28
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
29
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
30
- s.pod_target_xcconfig = {
31
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
32
- "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
33
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
34
- }
35
- s.dependency "React-Codegen"
36
- s.dependency "RCT-Folly"
37
- s.dependency "RCTRequired"
38
- s.dependency "RCTTypeSafety"
39
- s.dependency "ReactCommon/turbomodule/core"
40
- end
41
- end
42
-
43
- s.dependency "React-RCTAppDelegate"
44
23
  # DashX iOS SDK — consumers must provide the source in their Podfile:
45
24
  # pod 'DashX', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.1.9'
46
25
  # For local development:
package/README.md CHANGED
@@ -26,7 +26,9 @@ _DashX SDK for React Native_
26
26
 
27
27
  ## Install
28
28
 
29
- Requires **React Native 0.71+**, **iOS 13.0+**, **Android SDK 26+**, and **Node.js 16+**.
29
+ Requires **React Native 0.74+**, **iOS 13.0+**, **Android SDK 26+**, and **Node.js 16+**.
30
+
31
+ Supports both the **Old Architecture** (bridge) and **New Architecture** (TurboModule) from a single install.
30
32
 
31
33
  > **Note:** This SDK uses native modules and is **not compatible with Expo Go**. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) if you are using Expo.
32
34
 
@@ -0,0 +1,72 @@
1
+ const mockNativeModule = {
2
+ configure: jest.fn(),
3
+ identify: jest.fn(),
4
+ setIdentity: jest.fn(),
5
+ reset: jest.fn(),
6
+ track: jest.fn(),
7
+ screen: jest.fn(),
8
+ fetchRecord: jest.fn(),
9
+ searchRecords: jest.fn(),
10
+ fetchStoredPreferences: jest.fn(),
11
+ saveStoredPreferences: jest.fn(),
12
+ subscribe: jest.fn(),
13
+ unsubscribe: jest.fn(),
14
+ setLogLevel: jest.fn(),
15
+ uploadAsset: jest.fn(),
16
+ fetchAsset: jest.fn(),
17
+ enableLifecycleTracking: jest.fn(),
18
+ enableAdTracking: jest.fn(),
19
+ processURL: jest.fn(),
20
+ trackNotificationNavigation: jest.fn(),
21
+ requestNotificationPermission: jest.fn(),
22
+ getNotificationPermissionStatus: jest.fn(),
23
+ addListener: jest.fn(),
24
+ removeListeners: jest.fn(),
25
+ };
26
+
27
+ jest.mock('react-native', () => ({
28
+ NativeModules: {
29
+ DashXReactNative: mockNativeModule,
30
+ },
31
+ NativeEventEmitter: jest.fn().mockImplementation(() => ({
32
+ addListener: jest.fn(() => ({ remove: jest.fn() })),
33
+ })),
34
+ Platform: {
35
+ OS: 'ios',
36
+ select: jest.fn((obj) => obj.default || ''),
37
+ },
38
+ TurboModuleRegistry: {
39
+ get: jest.fn(() => null),
40
+ getEnforcing: jest.fn(() => {
41
+ throw new Error('not available');
42
+ }),
43
+ },
44
+ }));
45
+
46
+ // TurboModule returns null -> should fall back to NativeModules
47
+ jest.mock('../src/NativeDashXReactNative', () => ({
48
+ __esModule: true,
49
+ default: null,
50
+ }));
51
+
52
+ const DashX = require('../index').default;
53
+
54
+ describe('NativeModules fallback', () => {
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ });
58
+
59
+ it('falls back to NativeModules.DashXReactNative when TurboModule is null', () => {
60
+ DashX.configure({ publicKey: 'pk_test' });
61
+ expect(mockNativeModule.configure).toHaveBeenCalledWith({
62
+ publicKey: 'pk_test',
63
+ });
64
+ });
65
+
66
+ it('calls track on the fallback module', () => {
67
+ DashX.track('event_name', { key: 'value' });
68
+ expect(mockNativeModule.track).toHaveBeenCalledWith('event_name', {
69
+ key: 'value',
70
+ });
71
+ });
72
+ });
@@ -37,6 +37,15 @@ jest.mock('react-native', () => ({
37
37
  OS: 'ios',
38
38
  select: jest.fn((obj) => obj.default || ''),
39
39
  },
40
+ TurboModuleRegistry: {
41
+ get: jest.fn(() => mockNativeModule),
42
+ getEnforcing: jest.fn(() => mockNativeModule),
43
+ },
44
+ }));
45
+
46
+ jest.mock('../src/NativeDashXReactNative', () => ({
47
+ __esModule: true,
48
+ default: mockNativeModule,
40
49
  }));
41
50
 
42
51
  const DashX = require('../index').default;
@@ -101,10 +110,14 @@ describe('setIdentity', () => {
101
110
  expect(() => DashX.setIdentity(null, 'token')).toThrow('uid is required');
102
111
  });
103
112
 
104
- it('throws if token is missing', () => {
105
- expect(() => DashX.setIdentity('uid', null)).toThrow(
106
- 'token is required'
107
- );
113
+ it('passes null token through to native', () => {
114
+ DashX.setIdentity('uid', null);
115
+ expect(mockNativeModule.setIdentity).toHaveBeenCalledWith('uid', null);
116
+ });
117
+
118
+ it('passes undefined token as null to native', () => {
119
+ DashX.setIdentity('uid');
120
+ expect(mockNativeModule.setIdentity).toHaveBeenCalledWith('uid', null);
108
121
  });
109
122
  });
110
123
 
@@ -3,6 +3,14 @@ import groovy.json.JsonSlurper
3
3
  apply plugin: 'com.android.library'
4
4
  apply plugin: 'kotlin-android'
5
5
 
6
+ def isNewArchitectureEnabled() {
7
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
8
+ }
9
+
10
+ if (isNewArchitectureEnabled()) {
11
+ apply plugin: 'com.facebook.react'
12
+ }
13
+
6
14
  def getVersionFromNpm() {
7
15
  def inputFile = new File("../package.json")
8
16
  def packageJson = new JsonSlurper().parseText(inputFile.text)
@@ -30,6 +38,16 @@ android {
30
38
  abortOnError false
31
39
  }
32
40
 
41
+ sourceSets {
42
+ main {
43
+ if (isNewArchitectureEnabled()) {
44
+ java.srcDirs += ['src/newarch/java']
45
+ } else {
46
+ java.srcDirs += ['src/oldarch/java']
47
+ }
48
+ }
49
+ }
50
+
33
51
  packaging {
34
52
  pickFirst 'lib/arm64-v8a/libc++_shared.so'
35
53
  pickFirst 'lib/arm64-v8a/libfbjni.so'
@@ -42,6 +60,14 @@ android {
42
60
  }
43
61
  }
44
62
 
63
+ if (isNewArchitectureEnabled()) {
64
+ react {
65
+ jsRootDir = file("../src/")
66
+ libraryName = "RNDashXReactNativeSpec"
67
+ codegenJavaPackageName = "com.dashx.rn.sdk"
68
+ }
69
+ }
70
+
45
71
  repositories {
46
72
  mavenCentral()
47
73
  google()
@@ -55,5 +81,5 @@ dependencies {
55
81
  // Ensure Firebase Messaging types are available for FCM service subclass
56
82
  implementation 'com.google.firebase:firebase-messaging-ktx:23.1.2'
57
83
 
58
- implementation "com.dashx:dashx-android:1.2.4"
84
+ implementation "com.dashx:dashx-android:1.2.5"
59
85
  }
@@ -1,5 +1,8 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
2
  xmlns:tools="http://schemas.android.com/tools">
3
+
4
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
5
+
3
6
  <application>
4
7
  <!-- Replace dashx-android's FCM service with our subclass that also emits to JS. -->
5
8
  <service
@@ -0,0 +1,384 @@
1
+ package com.dashx.rn.sdk
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.app.NotificationChannel
6
+ import android.app.NotificationManager
7
+ import android.content.Context
8
+ import android.content.pm.PackageManager
9
+ import android.os.Build
10
+ import androidx.core.app.ActivityCompat
11
+ import androidx.core.content.ContextCompat
12
+ import com.dashx.rn.sdk.util.*
13
+ import com.dashx.android.DashX
14
+ import com.dashx.android.DashXLog
15
+ import com.dashx.android.DashXLog.LogLevel
16
+ import com.dashx.android.data.Asset
17
+ import com.facebook.react.bridge.*
18
+ import com.facebook.react.modules.core.PermissionAwareActivity
19
+ import com.facebook.react.modules.core.PermissionListener
20
+ import kotlinx.serialization.decodeFromString
21
+ import kotlinx.serialization.encodeToString
22
+ import kotlinx.serialization.json.Json
23
+ import kotlinx.serialization.json.JsonObject
24
+
25
+ class DashXReactNativeModuleImpl(private val reactContext: ReactApplicationContext) {
26
+ private val tag = DashXReactNativeModuleImpl::class.java.simpleName
27
+
28
+ fun initialize() {
29
+ MessageReceivedEventHolder.reactContext = reactContext
30
+ }
31
+
32
+ fun invalidate() {
33
+ MessageReceivedEventHolder.reactContext = null
34
+ }
35
+
36
+ fun setLogLevel(level: Double) {
37
+ // JS contract: 0 = off, 1 = errors, 2 = debug
38
+ // Native SDK: OFF = -1, ERROR = 0, INFO = 1, DEBUG = 2
39
+ val logLevel = when (level.toInt()) {
40
+ 0 -> LogLevel.OFF
41
+ 1 -> LogLevel.ERROR
42
+ 2 -> LogLevel.DEBUG
43
+ else -> LogLevel.OFF
44
+ }
45
+ DashXLog.setLogLevel(logLevel)
46
+ }
47
+
48
+ fun configure(options: ReadableMap) {
49
+ createDefaultNotificationChannel()
50
+ DashX.configure(
51
+ reactContext,
52
+ options.getString("publicKey")!!,
53
+ options.getStringIfPresent("baseURI"),
54
+ options.getStringIfPresent("targetEnvironment")
55
+ )
56
+ }
57
+
58
+ private fun createDefaultNotificationChannel() {
59
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
60
+ val notificationManager = reactContext.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager ?: return
61
+
62
+ val existing = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID)
63
+ if (existing != null) return
64
+
65
+ val channel = NotificationChannel(
66
+ DEFAULT_CHANNEL_ID,
67
+ DEFAULT_CHANNEL_NAME,
68
+ NotificationManager.IMPORTANCE_HIGH
69
+ ).apply {
70
+ enableVibration(true)
71
+ setShowBadge(true)
72
+ lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
73
+ }
74
+ notificationManager.createNotificationChannel(channel)
75
+ }
76
+
77
+ fun identify(options: ReadableMap?) {
78
+ val optionsHashMap = options?.toHashMap()
79
+ ?.filterValues { it != null }
80
+ ?.mapValues { it.value.toString() }
81
+ DashX.identify(optionsHashMap?.let { HashMap(it) })
82
+ }
83
+
84
+ fun setIdentity(uid: String?, token: String?) {
85
+ DashX.setIdentity(uid, token)
86
+ }
87
+
88
+ fun reset() {
89
+ DashX.reset()
90
+ }
91
+
92
+ fun track(event: String, data: ReadableMap?) {
93
+ val jsonData = try {
94
+ data?.toHashMap()
95
+ ?.filterValues { it != null }
96
+ ?.mapValues { it.value.toString() }
97
+ ?.let { HashMap(it) }
98
+ } catch (e: Exception) {
99
+ DashXLog.d(tag, "Failed to convert track data, sending event without data: ${e.message}")
100
+ null
101
+ }
102
+
103
+ DashX.track(event, jsonData)
104
+ }
105
+
106
+ fun screen(screenName: String, data: ReadableMap?) {
107
+ val screenData = data?.toHashMap()
108
+ ?.filterValues { it != null }
109
+ ?.mapValues { it.value.toString() }
110
+ ?.let { HashMap(it) }
111
+ ?: HashMap()
112
+ DashX.screen(screenName, screenData)
113
+ }
114
+
115
+ fun fetchRecord(urn: String, options: ReadableMap?, promise: Promise) {
116
+ val preview = options?.getBooleanIfPresent("preview")
117
+ val language = options?.getStringIfPresent("language")
118
+ val fields = try {
119
+ convertReadableArrayToKJsonList(options?.getArray("fields"))
120
+ } catch (e: Exception) {
121
+ DashXLog.d(tag, e.message)
122
+ promise.reject(E_UNSPECIFIED, "Error parsing fields: ${e.message}")
123
+ return
124
+ }
125
+ val include = try {
126
+ convertReadableArrayToKJsonList(options?.getArray("include"))
127
+ } catch (e: Exception) {
128
+ DashXLog.d(tag, e.message)
129
+ promise.reject(E_UNSPECIFIED, "Error parsing include: ${e.message}")
130
+ return
131
+ }
132
+ val exclude = try {
133
+ convertReadableArrayToKJsonList(options?.getArray("exclude"))
134
+ } catch (e: Exception) {
135
+ DashXLog.d(tag, e.message)
136
+ promise.reject(E_UNSPECIFIED, "Error parsing exclude: ${e.message}")
137
+ return
138
+ }
139
+
140
+ DashX.fetchRecord(
141
+ urn = urn,
142
+ preview = preview,
143
+ language = language,
144
+ fields = fields,
145
+ include = include,
146
+ exclude = exclude,
147
+ onError = {
148
+ promise.reject(E_UNSPECIFIED, it.message)
149
+ },
150
+ onSuccess = {
151
+ promise.resolve(convertJsonToMap(convertKJsonToJson(it)))
152
+ }
153
+ )
154
+ }
155
+
156
+ fun searchRecords(resource: String, options: ReadableMap?, promise: Promise) {
157
+ val jsonFilter = try {
158
+ convertMapToJson(options?.getMap("filter"))?.let { convertJsonToKJson(it) }
159
+ } catch (e: Exception) {
160
+ DashXLog.d(tag, e.message)
161
+ promise.reject(E_UNSPECIFIED, "Error parsing filter: ${e.message}")
162
+ return
163
+ }
164
+ val order = try {
165
+ convertReadableArrayToKJsonList(options?.getArray("order"))
166
+ } catch (e: Exception) {
167
+ DashXLog.d(tag, e.message)
168
+ promise.reject(E_UNSPECIFIED, "Error parsing order: ${e.message}")
169
+ return
170
+ }
171
+ val limit = options?.getIntIfPresent("limit")
172
+ val preview = options?.getBooleanIfPresent("preview")
173
+ val language = options?.getStringIfPresent("language")
174
+ val fields = try {
175
+ convertReadableArrayToKJsonList(options?.getArray("fields"))
176
+ } catch (e: Exception) {
177
+ DashXLog.d(tag, e.message)
178
+ promise.reject(E_UNSPECIFIED, "Error parsing fields: ${e.message}")
179
+ return
180
+ }
181
+ val include = try {
182
+ convertReadableArrayToKJsonList(options?.getArray("include"))
183
+ } catch (e: Exception) {
184
+ DashXLog.d(tag, e.message)
185
+ promise.reject(E_UNSPECIFIED, "Error parsing include: ${e.message}")
186
+ return
187
+ }
188
+ val exclude = try {
189
+ convertReadableArrayToKJsonList(options?.getArray("exclude"))
190
+ } catch (e: Exception) {
191
+ DashXLog.d(tag, e.message)
192
+ promise.reject(E_UNSPECIFIED, "Error parsing exclude: ${e.message}")
193
+ return
194
+ }
195
+
196
+ DashX.searchRecords(
197
+ resource = resource,
198
+ filter = jsonFilter,
199
+ order = order,
200
+ limit = limit,
201
+ preview = preview,
202
+ language = language,
203
+ fields = fields,
204
+ include = include,
205
+ exclude = exclude,
206
+ onError = {
207
+ promise.reject(E_UNSPECIFIED, it.message)
208
+ },
209
+ onSuccess = { records ->
210
+ val readableArray = Arguments.createArray()
211
+ records.forEach {
212
+ readableArray.pushMap(convertJsonToMap(convertKJsonToJson(it)))
213
+ }
214
+ promise.resolve(readableArray)
215
+ }
216
+ )
217
+ }
218
+
219
+ fun fetchStoredPreferences(promise: Promise) {
220
+ DashX.fetchStoredPreferences(
221
+ onError = {
222
+ promise.reject(E_UNSPECIFIED, it.message)
223
+ },
224
+ onSuccess = { content ->
225
+ promise.resolve(convertJsonToMap(convertKJsonToJson(Json.decodeFromString<JsonObject>(Json.encodeToString(content)))))
226
+ }
227
+ )
228
+ }
229
+
230
+ fun saveStoredPreferences(preferenceData: ReadableMap?, promise: Promise) {
231
+ val preferencesString = MapUtil.toJSONElement(preferenceData).toString()
232
+
233
+ DashX.saveStoredPreferences(
234
+ Json.decodeFromString<JsonObject>(preferencesString),
235
+ onError = {
236
+ promise.reject(E_UNSPECIFIED, it.message)
237
+ },
238
+ onSuccess = { content ->
239
+ promise.resolve(convertJsonToMap(convertKJsonToJson(Json.decodeFromString<JsonObject>(Json.encodeToString(content)))))
240
+ }
241
+ )
242
+ }
243
+
244
+ fun subscribe() {
245
+ DashX.subscribe()
246
+ }
247
+
248
+ fun unsubscribe() {
249
+ DashX.unsubscribe()
250
+ }
251
+
252
+ // --- iOS-only methods: no-op stubs on Android (JS guards prevent these from being called) ---
253
+
254
+ fun enableLifecycleTracking() {
255
+ // iOS-only; no-op on Android
256
+ }
257
+
258
+ fun enableAdTracking() {
259
+ // iOS-only; no-op on Android
260
+ }
261
+
262
+ fun processURL(url: String, source: String?, forwardToLinkHandler: Boolean) {
263
+ // iOS-only; no-op on Android
264
+ }
265
+
266
+ fun trackNotificationNavigation(action: ReadableMap?, notificationId: String?) {
267
+ // iOS-only; no-op on Android
268
+ }
269
+
270
+ fun uploadAsset(filePath: String, resource: String, attribute: String, promise: Promise) {
271
+ val file = java.io.File(filePath)
272
+ DashX.uploadAsset(
273
+ file = file,
274
+ resource = resource,
275
+ attribute = attribute,
276
+ onSuccess = { asset ->
277
+ try {
278
+ val jsonString = Json.encodeToString(asset)
279
+ val jsonObject = org.json.JSONObject(jsonString)
280
+ promise.resolve(convertJsonToMap(jsonObject))
281
+ } catch (e: Exception) {
282
+ promise.resolve(null)
283
+ }
284
+ },
285
+ onError = {
286
+ promise.reject(E_UNSPECIFIED, it.message)
287
+ }
288
+ )
289
+ }
290
+
291
+ fun fetchAsset(assetId: String, promise: Promise) {
292
+ DashX.fetchAsset(
293
+ assetId = assetId,
294
+ onSuccess = { asset ->
295
+ try {
296
+ val jsonString = Json.encodeToString(asset)
297
+ val jsonObject = org.json.JSONObject(jsonString)
298
+ promise.resolve(convertJsonToMap(jsonObject))
299
+ } catch (e: Exception) {
300
+ promise.resolve(null)
301
+ }
302
+ },
303
+ onError = {
304
+ promise.reject(E_UNSPECIFIED, it.message)
305
+ }
306
+ )
307
+ }
308
+
309
+ fun requestNotificationPermission(promise: Promise) {
310
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
311
+ promise.resolve(PERMISSION_AUTHORIZED)
312
+ return
313
+ }
314
+
315
+ val alreadyGranted = ContextCompat.checkSelfPermission(
316
+ reactContext,
317
+ Manifest.permission.POST_NOTIFICATIONS
318
+ ) == PackageManager.PERMISSION_GRANTED
319
+ if (alreadyGranted) {
320
+ promise.resolve(PERMISSION_AUTHORIZED)
321
+ return
322
+ }
323
+
324
+ val activity = reactContext.currentActivity as? PermissionAwareActivity
325
+ if (activity == null) {
326
+ promise.reject(E_UNSPECIFIED, "No current activity to request permission from")
327
+ return
328
+ }
329
+
330
+ reactContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
331
+ .edit()
332
+ .putBoolean(KEY_PERMISSION_REQUESTED, true)
333
+ .apply()
334
+
335
+ val listener = PermissionListener { requestCode, _, grantResults ->
336
+ if (requestCode != NOTIFICATION_PERMISSION_REQUEST_CODE) return@PermissionListener false
337
+ val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
338
+ promise.resolve(if (granted) PERMISSION_AUTHORIZED else PERMISSION_DENIED)
339
+ true
340
+ }
341
+
342
+ activity.requestPermissions(
343
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
344
+ NOTIFICATION_PERMISSION_REQUEST_CODE,
345
+ listener
346
+ )
347
+ }
348
+
349
+ fun getNotificationPermissionStatus(promise: Promise) {
350
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
351
+ promise.resolve(PERMISSION_AUTHORIZED)
352
+ return
353
+ }
354
+ val granted = ContextCompat.checkSelfPermission(
355
+ reactContext,
356
+ Manifest.permission.POST_NOTIFICATIONS
357
+ ) == PackageManager.PERMISSION_GRANTED
358
+ if (granted) {
359
+ promise.resolve(PERMISSION_AUTHORIZED)
360
+ } else {
361
+ val wasRequested = reactContext
362
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
363
+ .getBoolean(KEY_PERMISSION_REQUESTED, false)
364
+ promise.resolve(if (wasRequested) PERMISSION_DENIED else PERMISSION_NOT_DETERMINED)
365
+ }
366
+ }
367
+
368
+ companion object {
369
+ const val NAME = "DashXReactNative"
370
+ private const val E_UNSPECIFIED = "EUNSPECIFIED"
371
+ private const val DEFAULT_CHANNEL_ID = "default_dashx_notification_channel"
372
+ private const val DEFAULT_CHANNEL_NAME = "Notifications"
373
+
374
+ private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 3571
375
+
376
+ private const val PREFS_NAME = "dashx_rn_prefs"
377
+ private const val KEY_PERMISSION_REQUESTED = "notification_permission_requested"
378
+
379
+ // Mirrors iOS UNAuthorizationStatus values used by the JS layer.
380
+ private const val PERMISSION_NOT_DETERMINED = 0
381
+ private const val PERMISSION_DENIED = 1
382
+ private const val PERMISSION_AUTHORIZED = 2
383
+ }
384
+ }