@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.
- package/.DS_Store +0 -0
- package/.claude/settings.local.json +18 -1
- package/.cursor/plans/deep_linking_implementation_plan_303c2ca9.plan.md +216 -0
- package/.gitignore +2 -2
- package/DashXReactNative.podspec +5 -26
- package/README.md +10 -478
- package/__tests__/fallback.test.js +72 -0
- package/__tests__/index.test.js +147 -4
- package/android/build.gradle +27 -1
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/dashx/rn/sdk/DashXReactNativeModuleImpl.kt +384 -0
- package/android/src/newarch/java/com/dashx/rn/sdk/DashXReactNativeModule.kt +85 -0
- package/android/src/newarch/java/com/dashx/rn/sdk/DashXReactNativePackage.kt +33 -0
- package/android/src/oldarch/java/com/dashx/rn/sdk/DashXReactNativeModule.kt +106 -0
- package/android/src/oldarch/java/com/dashx/rn/sdk/DashXReactNativePackage.kt +33 -0
- package/index.d.ts +32 -1
- package/index.js +31 -8
- package/ios/DashXNotificationHandler.swift +222 -0
- package/ios/DashXReactNative-Bridging-Header.h +0 -2
- package/ios/DashXReactNative.mm +32 -9
- package/ios/DashXReactNative.swift +60 -18
- package/package.json +11 -2
- package/src/NativeDashXReactNative.ts +42 -0
- package/android/src/main/java/com/dashx/rn/sdk/DashXReactNativeModule.kt +0 -231
- package/android/src/main/java/com/dashx/rn/sdk/DashXReactNativePackage.kt +0 -18
- package/ios/DashXRCTAppDelegate.h +0 -3
- package/ios/DashXRCTAppDelegate.swift +0 -266
- package/ios/DashXReactNative.h +0 -1
package/__tests__/index.test.js
CHANGED
|
@@ -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('
|
|
102
|
-
|
|
103
|
-
|
|
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', () => {
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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
|
+
}
|