@dashx/react-native 1.1.4 → 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.
@@ -16,6 +16,8 @@ const mockNativeModule = {
16
16
  fetchAsset: jest.fn(),
17
17
  enableLifecycleTracking: jest.fn(),
18
18
  enableAdTracking: jest.fn(),
19
+ processURL: jest.fn(),
20
+ trackNotificationNavigation: jest.fn(),
19
21
  requestNotificationPermission: jest.fn(),
20
22
  getNotificationPermissionStatus: jest.fn(),
21
23
  addListener: jest.fn(),
@@ -32,8 +34,18 @@ jest.mock('react-native', () => ({
32
34
  addListener: mockAddListener,
33
35
  })),
34
36
  Platform: {
37
+ OS: 'ios',
35
38
  select: jest.fn((obj) => obj.default || ''),
36
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,
37
49
  }));
38
50
 
39
51
  const DashX = require('../index').default;
@@ -98,10 +110,14 @@ describe('setIdentity', () => {
98
110
  expect(() => DashX.setIdentity(null, 'token')).toThrow('uid is required');
99
111
  });
100
112
 
101
- it('throws if token is missing', () => {
102
- expect(() => DashX.setIdentity('uid', null)).toThrow(
103
- 'token is required'
104
- );
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);
105
121
  });
106
122
  });
107
123
 
@@ -325,6 +341,133 @@ describe('fetchAsset', () => {
325
341
  });
326
342
  });
327
343
 
344
+ // --- processURL ---
345
+
346
+ describe('processURL', () => {
347
+ it('calls native processURL with defaults', () => {
348
+ DashX.processURL('https://example.com/path');
349
+ expect(mockNativeModule.processURL).toHaveBeenCalledWith(
350
+ 'https://example.com/path',
351
+ null,
352
+ true
353
+ );
354
+ });
355
+
356
+ it('passes source option', () => {
357
+ DashX.processURL('https://example.com', { source: 'universal_link' });
358
+ expect(mockNativeModule.processURL).toHaveBeenCalledWith(
359
+ 'https://example.com',
360
+ 'universal_link',
361
+ true
362
+ );
363
+ });
364
+
365
+ it('passes forwardToLinkHandler as false', () => {
366
+ DashX.processURL('https://example.com', { forwardToLinkHandler: false });
367
+ expect(mockNativeModule.processURL).toHaveBeenCalledWith(
368
+ 'https://example.com',
369
+ null,
370
+ false
371
+ );
372
+ });
373
+
374
+ it('throws if url is missing', () => {
375
+ expect(() => DashX.processURL(null)).toThrow('url is required');
376
+ });
377
+
378
+ it('throws if url is not a string', () => {
379
+ expect(() => DashX.processURL(123)).toThrow(
380
+ 'url is required and must be a string'
381
+ );
382
+ });
383
+
384
+ it('no-ops on Android', () => {
385
+ const { Platform } = require('react-native');
386
+ Platform.OS = 'android';
387
+ DashX.processURL('https://example.com');
388
+ expect(mockNativeModule.processURL).not.toHaveBeenCalled();
389
+ Platform.OS = 'ios';
390
+ });
391
+ });
392
+
393
+ // --- trackNotificationNavigation ---
394
+
395
+ describe('trackNotificationNavigation', () => {
396
+ it('calls native with deepLink action', () => {
397
+ DashX.trackNotificationNavigation(
398
+ { type: 'deepLink', url: 'https://example.com' },
399
+ 'notif-1'
400
+ );
401
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
402
+ { type: 'deepLink', url: 'https://example.com' },
403
+ 'notif-1'
404
+ );
405
+ });
406
+
407
+ it('calls native with screen action', () => {
408
+ DashX.trackNotificationNavigation(
409
+ { type: 'screen', name: 'Home', data: { ref: 'banner' } },
410
+ 'notif-2'
411
+ );
412
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
413
+ { type: 'screen', name: 'Home', data: { ref: 'banner' } },
414
+ 'notif-2'
415
+ );
416
+ });
417
+
418
+ it('calls native with richLanding action', () => {
419
+ DashX.trackNotificationNavigation(
420
+ { type: 'richLanding', url: 'https://promo.example.com' },
421
+ 'notif-3'
422
+ );
423
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
424
+ { type: 'richLanding', url: 'https://promo.example.com' },
425
+ 'notif-3'
426
+ );
427
+ });
428
+
429
+ it('calls native with clickAction action', () => {
430
+ DashX.trackNotificationNavigation(
431
+ { type: 'clickAction', action: 'OPEN_SETTINGS' },
432
+ 'notif-4'
433
+ );
434
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
435
+ { type: 'clickAction', action: 'OPEN_SETTINGS' },
436
+ 'notif-4'
437
+ );
438
+ });
439
+
440
+ it('calls native with null action when omitted', () => {
441
+ DashX.trackNotificationNavigation(null, 'notif-5');
442
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
443
+ null,
444
+ 'notif-5'
445
+ );
446
+ });
447
+
448
+ it('calls native with null for both when both omitted', () => {
449
+ DashX.trackNotificationNavigation();
450
+ expect(mockNativeModule.trackNotificationNavigation).toHaveBeenCalledWith(
451
+ null,
452
+ null
453
+ );
454
+ });
455
+
456
+ it('throws if action is not an object', () => {
457
+ expect(() => DashX.trackNotificationNavigation('bad')).toThrow(
458
+ 'action must be an object or null'
459
+ );
460
+ });
461
+
462
+ it('no-ops on Android', () => {
463
+ const { Platform } = require('react-native');
464
+ Platform.OS = 'android';
465
+ DashX.trackNotificationNavigation({ type: 'deepLink', url: 'https://example.com' });
466
+ expect(mockNativeModule.trackNotificationNavigation).not.toHaveBeenCalled();
467
+ Platform.OS = 'ios';
468
+ });
469
+ });
470
+
328
471
  // --- requestNotificationPermission / getNotificationPermissionStatus ---
329
472
 
330
473
  describe('requestNotificationPermission', () => {
@@ -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.1.7"
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
+ }