@haseeba393/react-native-zendesk 2.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/LICENSE +21 -0
- package/README.md +171 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/rnzendesk/ZendeskHelpCenterViewManager.kt +38 -0
- package/android/src/main/java/com/rnzendesk/ZendeskModule.kt +341 -0
- package/android/src/main/java/com/rnzendesk/ZendeskPackage.kt +59 -0
- package/ios/RNZendeskHelpCenterView.h +9 -0
- package/ios/RNZendeskHelpCenterView.m +43 -0
- package/ios/RNZendeskHelpCenterViewManager.m +19 -0
- package/ios/RNZendeskModule.h +13 -0
- package/ios/RNZendeskModule.mm +515 -0
- package/package.json +63 -0
- package/react-native-zendesk.podspec +38 -0
- package/src/ZendeskHelpCenterView.tsx +13 -0
- package/src/fabric/ZendeskHelpCenterViewNativeComponent.ts +13 -0
- package/src/index.ts +57 -0
- package/src/specs/NativeZendesk.ts +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haseeb Ahmed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# react-native-zendesk
|
|
2
|
+
|
|
3
|
+
React Native Zendesk integration with **New Architecture** support (TurboModules + Fabric component). Use official Zendesk Support SDK UIs on iOS and Android.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Initialize Zendesk account config from JavaScript
|
|
8
|
+
- Initialize official Zendesk Support SDK (Android + iOS) when credentials are provided
|
|
9
|
+
- Open Help Center, articles, and contact support in native Zendesk UI (with browser fallback)
|
|
10
|
+
- Embed Help Center in your app with `ZendeskHelpCenterView`
|
|
11
|
+
- Custom fields support for contact requests
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- React Native 0.72+ with **New Architecture** enabled
|
|
16
|
+
- iOS 13.0+
|
|
17
|
+
- Android minSdkVersion 21+
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @haseeba393/react-native-zendesk
|
|
23
|
+
# or
|
|
24
|
+
yarn add @haseeba393/react-native-zendesk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### iOS
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd ios && pod install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Android
|
|
34
|
+
|
|
35
|
+
No additional setup required. Gradle will sync automatically.
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import {
|
|
41
|
+
initializeZendesk,
|
|
42
|
+
getZendeskArticles,
|
|
43
|
+
getZendeskArticle,
|
|
44
|
+
searchZendeskArticles,
|
|
45
|
+
createZendeskTicket,
|
|
46
|
+
openZendeskHelpCenter,
|
|
47
|
+
openZendeskArticle,
|
|
48
|
+
openZendeskContactSupport,
|
|
49
|
+
openZendeskContactSupportWithDetails,
|
|
50
|
+
ZendeskHelpCenterView,
|
|
51
|
+
} from '@haseeba393/react-native-zendesk';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Initialize
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
await initializeZendesk({
|
|
58
|
+
zendeskUrl: 'https://your-subdomain.zendesk.com', // required for native Zendesk SDK initialization
|
|
59
|
+
appId: 'zendesk_app_id', // required for native Zendesk SDK initialization
|
|
60
|
+
clientId: 'zendesk_client_id', // required for native Zendesk SDK initialization
|
|
61
|
+
// subdomain is optional when zendeskUrl is provided
|
|
62
|
+
// subdomain: 'your-subdomain',
|
|
63
|
+
name: 'John Appleseed', // optional identity
|
|
64
|
+
email: 'agent@example.com', // optional, required for authenticated APIs
|
|
65
|
+
apiToken: 'zendesk_api_token', // optional, required for authenticated APIs
|
|
66
|
+
locale: 'en-us',
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Open Help Center
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await openZendeskHelpCenter();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Open a specific article
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await openZendeskArticle(12345);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Get articles / Search / Create ticket
|
|
83
|
+
|
|
84
|
+
`getZendeskArticles`, `searchZendeskArticles`, and `createZendeskTicket` open the native Help Center or Contact Support UI. Use `openZendeskHelpCenter` or `openZendeskContactSupport` directly for the same behavior.
|
|
85
|
+
|
|
86
|
+
### Open contact support
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
await openZendeskContactSupport();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Open contact support with custom fields
|
|
93
|
+
|
|
94
|
+
Pass custom fields as key-value pairs. The `key` is the Zendesk custom field ID (as string, e.g. `"360035988993"`), and `value` is the field value.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import {
|
|
98
|
+
openZendeskContactSupportWithDetails,
|
|
99
|
+
type ZendeskCustomField,
|
|
100
|
+
} from '@haseeba393/react-native-zendesk';
|
|
101
|
+
|
|
102
|
+
await openZendeskContactSupportWithDetails('user@example.com', [
|
|
103
|
+
{ key: '360035988993', value: '1.0.0' }, // e.g. app version
|
|
104
|
+
{ key: '25024443', value: 'Diagnostic info' }, // e.g. diagnostic description
|
|
105
|
+
]);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Native Help Center view
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<ZendeskHelpCenterView
|
|
112
|
+
style={{ flex: 1 }}
|
|
113
|
+
url="https://your-subdomain.zendesk.com/hc/en-us"
|
|
114
|
+
javaScriptEnabled
|
|
115
|
+
/>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Behavior Notes
|
|
119
|
+
|
|
120
|
+
- **New Architecture required**: This package uses TurboModules and Fabric. Ensure `newArchEnabled` is `true` in your React Native config.
|
|
121
|
+
- **Native SDK vs browser**: When `zendeskUrl`, `appId`, and `clientId` are set in `initializeZendesk`, the native Zendesk SDK UIs are used. Otherwise, methods fall back to opening Help Center URLs in the browser.
|
|
122
|
+
- **Subdomain**: Optional if `zendeskUrl` is provided. It is auto-derived from `https://<subdomain>.zendesk.com`.
|
|
123
|
+
- **Custom field IDs**: Find your Zendesk custom field IDs in Admin Center → Objects and rules → Tickets → Fields.
|
|
124
|
+
|
|
125
|
+
## Example App
|
|
126
|
+
|
|
127
|
+
An example app is included in `example/` to test this package locally.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
cd example
|
|
131
|
+
npm install
|
|
132
|
+
cd ios && pod install && cd ..
|
|
133
|
+
npm run ios
|
|
134
|
+
# or
|
|
135
|
+
npm run android
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Create `example/zendesk.config.local.ts` with your Zendesk credentials for full testing (see `zendesk.config.local.example.ts`).
|
|
139
|
+
|
|
140
|
+
## Publishing
|
|
141
|
+
|
|
142
|
+
Before publishing to npm:
|
|
143
|
+
|
|
144
|
+
1. Add your name/email to `author` in `package.json`.
|
|
145
|
+
2. Update the `LICENSE` copyright year and holder if needed.
|
|
146
|
+
3. Run `npm pack` to verify the package contents.
|
|
147
|
+
|
|
148
|
+
### npm
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm login
|
|
152
|
+
npm publish --access public
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> Scoped packages (`@haseeba393/react-native-zendesk`) require `--access public` for the first publish.
|
|
156
|
+
|
|
157
|
+
### GitHub
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git init
|
|
161
|
+
git add .
|
|
162
|
+
git commit -m "Initial release"
|
|
163
|
+
git remote add origin https://github.com/Haseeba393/react-native-zendesk.git
|
|
164
|
+
git push -u origin main
|
|
165
|
+
git tag v0.1.0
|
|
166
|
+
git push origin v0.1.0
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext {
|
|
3
|
+
kotlinVersion = "1.9.24"
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
apply plugin: "com.android.library"
|
|
8
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
9
|
+
apply plugin: "com.facebook.react"
|
|
10
|
+
|
|
11
|
+
def safeExtGet(prop, fallback) {
|
|
12
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
android {
|
|
16
|
+
namespace "com.rnzendesk"
|
|
17
|
+
compileSdk safeExtGet("compileSdkVersion", 34)
|
|
18
|
+
|
|
19
|
+
defaultConfig {
|
|
20
|
+
minSdk safeExtGet("minSdkVersion", 24)
|
|
21
|
+
targetSdk safeExtGet("targetSdkVersion", 34)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
buildFeatures {
|
|
25
|
+
buildConfig true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
compileOptions {
|
|
29
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
30
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
kotlinOptions {
|
|
34
|
+
jvmTarget = "17"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
react {
|
|
39
|
+
jsRootDir = file("../src")
|
|
40
|
+
libraryName = "RNZendeskSpec"
|
|
41
|
+
codegenJavaPackageName = "com.rnzendesk"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
repositories {
|
|
45
|
+
maven { url "https://zendesk.jfrog.io/zendesk/repo" }
|
|
46
|
+
mavenCentral()
|
|
47
|
+
google()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
dependencies {
|
|
51
|
+
implementation "com.facebook.react:react-android"
|
|
52
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
|
53
|
+
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
54
|
+
implementation "com.zendesk:support:5.5.2"
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest package="com.rnzendesk" />
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.rnzendesk
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.webkit.WebSettings
|
|
5
|
+
import android.webkit.WebView
|
|
6
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
7
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
|
+
|
|
10
|
+
class ZendeskHelpCenterViewManager : SimpleViewManager<WebView>() {
|
|
11
|
+
companion object {
|
|
12
|
+
const val REACT_CLASS = "RNZendeskHelpCenterView"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override fun getName(): String = REACT_CLASS
|
|
16
|
+
|
|
17
|
+
@SuppressLint("SetJavaScriptEnabled")
|
|
18
|
+
override fun createViewInstance(reactContext: ThemedReactContext): WebView {
|
|
19
|
+
return WebView(reactContext).apply {
|
|
20
|
+
settings.javaScriptEnabled = true
|
|
21
|
+
settings.domStorageEnabled = true
|
|
22
|
+
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@ReactProp(name = "url")
|
|
27
|
+
fun setUrl(view: WebView, url: String?) {
|
|
28
|
+
if (url.isNullOrBlank()) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
view.loadUrl(url)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@ReactProp(name = "javaScriptEnabled", defaultBoolean = true)
|
|
35
|
+
fun setJavaScriptEnabled(view: WebView, enabled: Boolean) {
|
|
36
|
+
view.settings.javaScriptEnabled = enabled
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
package com.rnzendesk
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReadableArray
|
|
9
|
+
import com.facebook.react.bridge.ReadableMap
|
|
10
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
11
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
12
|
+
import okhttp3.OkHttpClient
|
|
13
|
+
import okhttp3.Request
|
|
14
|
+
import okhttp3.RequestBody.Companion.toRequestBody
|
|
15
|
+
import org.json.JSONArray
|
|
16
|
+
import org.json.JSONObject
|
|
17
|
+
import java.io.IOException
|
|
18
|
+
import java.util.Base64
|
|
19
|
+
import java.util.concurrent.Executors
|
|
20
|
+
import zendesk.core.AnonymousIdentity
|
|
21
|
+
import zendesk.core.Zendesk
|
|
22
|
+
import zendesk.support.CustomField
|
|
23
|
+
import zendesk.support.Support
|
|
24
|
+
import zendesk.support.guide.HelpCenterActivity
|
|
25
|
+
import zendesk.support.guide.ViewArticleActivity
|
|
26
|
+
import zendesk.support.request.RequestActivity
|
|
27
|
+
import zendesk.support.requestlist.RequestListActivity
|
|
28
|
+
|
|
29
|
+
@ReactModule(name = ZendeskModule.NAME)
|
|
30
|
+
class ZendeskModule(reactContext: ReactApplicationContext) :
|
|
31
|
+
NativeZendeskSpec(reactContext) {
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
const val NAME = "RNZendesk"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private val httpClient = OkHttpClient()
|
|
38
|
+
private val executor = Executors.newSingleThreadExecutor()
|
|
39
|
+
|
|
40
|
+
private var subdomain: String? = null
|
|
41
|
+
private var email: String? = null
|
|
42
|
+
private var apiToken: String? = null
|
|
43
|
+
private var locale: String = ""
|
|
44
|
+
private var sdkInitialized: Boolean = false
|
|
45
|
+
|
|
46
|
+
override fun getName(): String = NAME
|
|
47
|
+
|
|
48
|
+
override fun initialize(config: ReadableMap, promise: Promise) {
|
|
49
|
+
val providedSubdomain = config.getString("subdomain")
|
|
50
|
+
email = config.getString("email")
|
|
51
|
+
apiToken = config.getString("apiToken")
|
|
52
|
+
locale = config.getString("locale") ?: ""
|
|
53
|
+
|
|
54
|
+
val zendeskUrl = config.getString("zendeskUrl")
|
|
55
|
+
val appId = config.getString("appId")
|
|
56
|
+
val clientId = config.getString("clientId")
|
|
57
|
+
val name = config.getString("name")
|
|
58
|
+
|
|
59
|
+
val derivedSubdomain = extractSubdomainFromZendeskUrl(zendeskUrl)
|
|
60
|
+
subdomain = if (!providedSubdomain.isNullOrBlank()) {
|
|
61
|
+
providedSubdomain
|
|
62
|
+
} else {
|
|
63
|
+
derivedSubdomain
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (subdomain.isNullOrBlank()) {
|
|
67
|
+
promise.reject(
|
|
68
|
+
"E_ZENDESK_CONFIG",
|
|
69
|
+
"Provide subdomain or a valid zendeskUrl (https://<subdomain>.zendesk.com)"
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sdkInitialized = false
|
|
75
|
+
if (!zendeskUrl.isNullOrBlank() && !appId.isNullOrBlank() && !clientId.isNullOrBlank()) {
|
|
76
|
+
try {
|
|
77
|
+
Zendesk.INSTANCE.init(reactApplicationContext, zendeskUrl, appId, clientId)
|
|
78
|
+
Support.INSTANCE.init(Zendesk.INSTANCE)
|
|
79
|
+
|
|
80
|
+
val identityBuilder = AnonymousIdentity.Builder()
|
|
81
|
+
if (!name.isNullOrBlank()) {
|
|
82
|
+
identityBuilder.withNameIdentifier(name)
|
|
83
|
+
}
|
|
84
|
+
if (!email.isNullOrBlank()) {
|
|
85
|
+
identityBuilder.withEmailIdentifier(email)
|
|
86
|
+
}
|
|
87
|
+
Zendesk.INSTANCE.setIdentity(identityBuilder.build())
|
|
88
|
+
|
|
89
|
+
if (locale.isNotBlank()) {
|
|
90
|
+
Support.INSTANCE.setHelpCenterLocaleOverride(java.util.Locale.forLanguageTag(locale))
|
|
91
|
+
}
|
|
92
|
+
sdkInitialized = true
|
|
93
|
+
} catch (error: Exception) {
|
|
94
|
+
promise.reject("E_ZENDESK_SDK_INIT", error.message, error)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
promise.resolve(true)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun getArticles(
|
|
102
|
+
locale: String?,
|
|
103
|
+
labels: ReadableArray?,
|
|
104
|
+
page: Double?,
|
|
105
|
+
perPage: Double?,
|
|
106
|
+
promise: Promise
|
|
107
|
+
) {
|
|
108
|
+
// Use native Help Center UI instead of REST APIs.
|
|
109
|
+
openHelpCenter(promise)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun getArticle(articleId: Double, locale: String?, promise: Promise) {
|
|
113
|
+
// Use native article UI instead of REST APIs.
|
|
114
|
+
openArticle(articleId, promise)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override fun searchArticles(
|
|
118
|
+
query: String,
|
|
119
|
+
locale: String?,
|
|
120
|
+
page: Double?,
|
|
121
|
+
perPage: Double?,
|
|
122
|
+
promise: Promise
|
|
123
|
+
) {
|
|
124
|
+
// Search is handled by native Help Center UI.
|
|
125
|
+
openHelpCenter(promise)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override fun createTicket(request: ReadableMap, promise: Promise) {
|
|
129
|
+
// Use native contact support UI instead of REST ticket APIs.
|
|
130
|
+
openContactSupport(promise)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override fun openHelpCenter(promise: Promise) {
|
|
134
|
+
val domain = requireSubdomain(promise) ?: return
|
|
135
|
+
val activity = currentActivity
|
|
136
|
+
if (sdkInitialized && activity != null) {
|
|
137
|
+
try {
|
|
138
|
+
HelpCenterActivity.builder().show(activity)
|
|
139
|
+
promise.resolve(true)
|
|
140
|
+
} catch (error: Exception) {
|
|
141
|
+
promise.reject("E_ZENDESK_OPEN_HELP_CENTER", error.message, error)
|
|
142
|
+
}
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
val localeSegment = if (locale.isNotBlank()) "/$locale" else ""
|
|
146
|
+
openUrl("https://$domain.zendesk.com/hc$localeSegment", promise)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun openArticle(articleId: Double, promise: Promise) {
|
|
150
|
+
val domain = requireSubdomain(promise) ?: return
|
|
151
|
+
val id = articleId.toLong()
|
|
152
|
+
val activity = currentActivity
|
|
153
|
+
if (sdkInitialized && activity != null) {
|
|
154
|
+
try {
|
|
155
|
+
ViewArticleActivity.builder(id).show(activity)
|
|
156
|
+
promise.resolve(true)
|
|
157
|
+
} catch (error: Exception) {
|
|
158
|
+
promise.reject("E_ZENDESK_OPEN_ARTICLE", error.message, error)
|
|
159
|
+
}
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
openUrl("https://$domain.zendesk.com/hc/articles/$id", promise)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
override fun openContactSupport(promise: Promise) {
|
|
166
|
+
val domain = requireSubdomain(promise) ?: return
|
|
167
|
+
val activity = currentActivity
|
|
168
|
+
if (sdkInitialized && activity != null) {
|
|
169
|
+
try {
|
|
170
|
+
RequestListActivity.builder().show(activity)
|
|
171
|
+
promise.resolve(true)
|
|
172
|
+
} catch (error: Exception) {
|
|
173
|
+
promise.reject("E_ZENDESK_OPEN_CONTACT_SUPPORT", error.message, error)
|
|
174
|
+
}
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
val localeSegment = if (locale.isNotBlank()) "/$locale" else ""
|
|
178
|
+
openUrl("https://$domain.zendesk.com/hc$localeSegment/requests/new", promise)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override fun openContactSupportWithDetails(
|
|
182
|
+
email: String,
|
|
183
|
+
customFields: ReadableArray,
|
|
184
|
+
promise: Promise
|
|
185
|
+
) {
|
|
186
|
+
val domain = requireSubdomain(promise) ?: return
|
|
187
|
+
val activity = currentActivity
|
|
188
|
+
|
|
189
|
+
if (sdkInitialized && activity != null) {
|
|
190
|
+
try {
|
|
191
|
+
if (email.isNotBlank()) {
|
|
192
|
+
val identity = AnonymousIdentity.Builder()
|
|
193
|
+
.withEmailIdentifier(email)
|
|
194
|
+
.build()
|
|
195
|
+
Zendesk.INSTANCE.setIdentity(identity)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
val fields = arrayListOf<CustomField>()
|
|
199
|
+
for (i in 0 until customFields.size()) {
|
|
200
|
+
val item = customFields.getMap(i)
|
|
201
|
+
val key = item.getString("key") ?: continue
|
|
202
|
+
val value = item.getString("value") ?: continue
|
|
203
|
+
val fieldId = key.trim().removeSuffix("L").toLongOrNull() ?: continue
|
|
204
|
+
fields.add(CustomField(fieldId, value))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
RequestActivity.builder()
|
|
208
|
+
.withCustomFields(fields)
|
|
209
|
+
.show(activity)
|
|
210
|
+
promise.resolve(true)
|
|
211
|
+
} catch (error: Exception) {
|
|
212
|
+
promise.reject("E_ZENDESK_OPEN_CONTACT_SUPPORT", error.message, error)
|
|
213
|
+
}
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
val localeSegment = if (locale.isNotBlank()) "/$locale" else ""
|
|
218
|
+
openUrl("https://$domain.zendesk.com/hc$localeSegment/requests/new", promise)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private fun openUrl(url: String, promise: Promise) {
|
|
222
|
+
try {
|
|
223
|
+
val activity = currentActivity
|
|
224
|
+
if (activity == null) {
|
|
225
|
+
promise.reject("E_ZENDESK_ACTIVITY", "No active activity available")
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
230
|
+
activity.startActivity(intent)
|
|
231
|
+
promise.resolve(true)
|
|
232
|
+
} catch (error: Exception) {
|
|
233
|
+
promise.reject("E_ZENDESK_OPEN_URL", error.message, error)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun request(method: String, url: String, payload: String?, promise: Promise) {
|
|
238
|
+
executor.execute {
|
|
239
|
+
try {
|
|
240
|
+
val requestBuilder = Request.Builder().url(url)
|
|
241
|
+
authHeader()?.let { requestBuilder.addHeader("Authorization", it) }
|
|
242
|
+
requestBuilder.addHeader("Accept", "application/json")
|
|
243
|
+
|
|
244
|
+
if (method == "POST") {
|
|
245
|
+
val body = (payload ?: "{}").toRequestBody("application/json".toMediaType())
|
|
246
|
+
requestBuilder.post(body)
|
|
247
|
+
} else {
|
|
248
|
+
requestBuilder.get()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
httpClient.newCall(requestBuilder.build()).execute().use { response ->
|
|
252
|
+
val body = response.body?.string() ?: "{}"
|
|
253
|
+
if (!response.isSuccessful) {
|
|
254
|
+
promise.reject(
|
|
255
|
+
"E_ZENDESK_HTTP",
|
|
256
|
+
"Zendesk request failed (${response.code}): $body"
|
|
257
|
+
)
|
|
258
|
+
return@use
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
val json = JSONObject(body)
|
|
262
|
+
val map = Arguments.makeNativeMap(json.toMap())
|
|
263
|
+
promise.resolve(map)
|
|
264
|
+
}
|
|
265
|
+
} catch (error: IOException) {
|
|
266
|
+
promise.reject("E_ZENDESK_NETWORK", error.message, error)
|
|
267
|
+
} catch (error: Exception) {
|
|
268
|
+
promise.reject("E_ZENDESK_UNKNOWN", error.message, error)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private fun JSONObject.toMap(): MutableMap<String, Any?> {
|
|
274
|
+
val map = mutableMapOf<String, Any?>()
|
|
275
|
+
keys().forEach { key ->
|
|
276
|
+
map[key] = when (val value = get(key)) {
|
|
277
|
+
is JSONArray -> value.toList()
|
|
278
|
+
is JSONObject -> value.toMap()
|
|
279
|
+
JSONObject.NULL -> null
|
|
280
|
+
else -> value
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return map
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private fun JSONArray.toList(): List<Any?> {
|
|
287
|
+
val list = mutableListOf<Any?>()
|
|
288
|
+
for (index in 0 until length()) {
|
|
289
|
+
val value = get(index)
|
|
290
|
+
list.add(
|
|
291
|
+
when (value) {
|
|
292
|
+
is JSONArray -> value.toList()
|
|
293
|
+
is JSONObject -> value.toMap()
|
|
294
|
+
JSONObject.NULL -> null
|
|
295
|
+
else -> value
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
return list
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private fun configuredApiBaseUrl(promise: Promise): String? {
|
|
303
|
+
val domain = requireSubdomain(promise) ?: return null
|
|
304
|
+
return "https://$domain.zendesk.com/api/v2"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private fun effectiveLocale(requestedLocale: String?): String = requestedLocale ?: locale
|
|
308
|
+
|
|
309
|
+
private fun authHeader(): String? {
|
|
310
|
+
if (email.isNullOrBlank() || apiToken.isNullOrBlank()) {
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
val token = "${email}/token:$apiToken"
|
|
314
|
+
val encoded = Base64.getEncoder().encodeToString(token.toByteArray(Charsets.UTF_8))
|
|
315
|
+
return "Basic $encoded"
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private fun requireSubdomain(promise: Promise): String? {
|
|
319
|
+
val domain = subdomain
|
|
320
|
+
if (domain.isNullOrBlank()) {
|
|
321
|
+
promise.reject("E_ZENDESK_CONFIG", "Zendesk is not initialized. Call initialize() first.")
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
return domain
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private fun extractSubdomainFromZendeskUrl(zendeskUrl: String?): String? {
|
|
328
|
+
if (zendeskUrl.isNullOrBlank()) {
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
return try {
|
|
332
|
+
val host = Uri.parse(zendeskUrl).host ?: return null
|
|
333
|
+
if (!host.endsWith(".zendesk.com")) {
|
|
334
|
+
return null
|
|
335
|
+
}
|
|
336
|
+
host.substringBefore(".zendesk.com").takeIf { it.isNotBlank() }
|
|
337
|
+
} catch (_: Exception) {
|
|
338
|
+
null
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.rnzendesk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.TurboReactPackage
|
|
4
|
+
import com.facebook.react.ViewManagerOnDemandReactPackage
|
|
5
|
+
import com.facebook.react.bridge.NativeModule
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
8
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
9
|
+
import com.facebook.react.uimanager.ViewManager
|
|
10
|
+
|
|
11
|
+
class ZendeskPackage :
|
|
12
|
+
TurboReactPackage(),
|
|
13
|
+
ViewManagerOnDemandReactPackage {
|
|
14
|
+
|
|
15
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
16
|
+
return if (name == ZendeskModule.NAME) {
|
|
17
|
+
ZendeskModule(reactContext)
|
|
18
|
+
} else {
|
|
19
|
+
null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
24
|
+
return ReactModuleInfoProvider {
|
|
25
|
+
mapOf(
|
|
26
|
+
ZendeskModule.NAME to ReactModuleInfo(
|
|
27
|
+
ZendeskModule.NAME,
|
|
28
|
+
ZendeskModule::class.java.name,
|
|
29
|
+
false,
|
|
30
|
+
false,
|
|
31
|
+
true,
|
|
32
|
+
false,
|
|
33
|
+
true
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun createViewManagers(
|
|
40
|
+
reactContext: ReactApplicationContext
|
|
41
|
+
): List<ViewManager<*, *>> {
|
|
42
|
+
return listOf(ZendeskHelpCenterViewManager())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun getViewManagerNames(reactContext: ReactApplicationContext): MutableList<String> {
|
|
46
|
+
return mutableListOf(ZendeskHelpCenterViewManager.REACT_CLASS)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun createViewManager(
|
|
50
|
+
reactContext: ReactApplicationContext,
|
|
51
|
+
viewManagerName: String
|
|
52
|
+
): ViewManager<*, *>? {
|
|
53
|
+
return if (viewManagerName == ZendeskHelpCenterViewManager.REACT_CLASS) {
|
|
54
|
+
ZendeskHelpCenterViewManager()
|
|
55
|
+
} else {
|
|
56
|
+
null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|