react-native-hubspot-wrapper 0.1.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/HUBSPOT_IOS_SDK_VERSION.json +5 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +19 -0
- package/README.md +119 -0
- package/ReactNativeHubspotWrapper.podspec +27 -0
- package/android/build.gradle +49 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotWrapperModule.kt +83 -0
- package/android/src/main/java/com/reactnativehubspotwrapper/HubspotWrapperPackage.kt +32 -0
- package/ios/HubspotMobileSDK/API/APIModels.swift +50 -0
- package/ios/HubspotMobileSDK/API/HubspotAPI.swift +168 -0
- package/ios/HubspotMobileSDK/DeviceTokenSyncState.swift +13 -0
- package/ios/HubspotMobileSDK/HubspotConfig.swift +145 -0
- package/ios/HubspotMobileSDK/HubspotManager+Notifications.swift +178 -0
- package/ios/HubspotMobileSDK/HubspotManager+Properties.swift +53 -0
- package/ios/HubspotMobileSDK/HubspotManager.swift +548 -0
- package/ios/HubspotMobileSDK/HubspotMobileSDK.swift +7 -0
- package/ios/HubspotMobileSDK/HubspotUserProperties.swift +115 -0
- package/ios/HubspotMobileSDK/LICENSE.txt +19 -0
- package/ios/HubspotMobileSDK/PushNotificationChatData.swift +63 -0
- package/ios/HubspotMobileSDK/Resources/Images.xcassets/Contents.json +6 -0
- package/ios/HubspotMobileSDK/Resources/Images.xcassets/GenericChatIcon.imageset/Contents.json +16 -0
- package/ios/HubspotMobileSDK/Resources/Images.xcassets/GenericChatIcon.imageset/chat-open-svg.svg +1 -0
- package/ios/HubspotMobileSDK/Resources/Localizable.xcstrings +28 -0
- package/ios/HubspotMobileSDK/Resources/PrivacyInfo.xcprivacy +62 -0
- package/ios/HubspotMobileSDK/Views/Buttons/FloatingActionButton.swift +126 -0
- package/ios/HubspotMobileSDK/Views/Buttons/TextChatButtonChatButton.swift +78 -0
- package/ios/HubspotMobileSDK/Views/ChatView/HubspotChatView.swift +612 -0
- package/ios/HubspotWrapperImpl.swift +108 -0
- package/ios/RNHubspotWrapper.h +9 -0
- package/ios/RNHubspotWrapper.mm +66 -0
- package/package.json +55 -0
- package/react-native.config.js +11 -0
- package/scripts/update-hubspot-ios-sdk.sh +142 -0
- package/src/index.ts +41 -0
- package/src/specs/NativeHubspotWrapper.ts +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcin Olek
|
|
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/MIGRATION.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Migration from react-native-hubspot-chat
|
|
2
|
+
|
|
3
|
+
## Import change
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// before
|
|
7
|
+
import HubspotChat from 'react-native-hubspot-chat';
|
|
8
|
+
|
|
9
|
+
// after
|
|
10
|
+
import HubspotWrapper from 'react-native-hubspot-wrapper';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API mapping
|
|
14
|
+
|
|
15
|
+
- `HubspotChat.init()` -> `HubspotWrapper.initialize()`
|
|
16
|
+
- `HubspotChat.open(chatflow)` -> `HubspotWrapper.openChat(chatflow)`
|
|
17
|
+
- `HubspotChat.identify(token, email)` -> `HubspotWrapper.setIdentity({ identityToken: token, email })`
|
|
18
|
+
- `HubspotChat.setProperties(properties)` -> `HubspotWrapper.setProperties(properties)`
|
|
19
|
+
- `HubspotChat.endSession()` -> `HubspotWrapper.clearUserData()`
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# react-native-hubspot-wrapper
|
|
2
|
+
|
|
3
|
+
TurboModule-only React Native wrapper for the HubSpot Mobile Chat SDK.
|
|
4
|
+
|
|
5
|
+
> Unofficial. Not affiliated with, endorsed by, or sponsored by HubSpot, Inc.
|
|
6
|
+
> "HubSpot" is a trademark of HubSpot, Inc.
|
|
7
|
+
|
|
8
|
+
## Status
|
|
9
|
+
|
|
10
|
+
Early release (`0.x`). The public API may evolve before `1.0`.
|
|
11
|
+
|
|
12
|
+
Currently supported:
|
|
13
|
+
|
|
14
|
+
- `initialize()`
|
|
15
|
+
- `openChat(chatflow)`
|
|
16
|
+
- `setIdentity({ identityToken, email? })`
|
|
17
|
+
- `setProperties(properties)`
|
|
18
|
+
- `clearUserData()`
|
|
19
|
+
|
|
20
|
+
Not yet implemented (planned):
|
|
21
|
+
|
|
22
|
+
- Push notifications (FCM token registration on Android, APNs token on iOS,
|
|
23
|
+
deep-linking from a notification into the relevant chat)
|
|
24
|
+
- Chat lifecycle events (open / close / message-received callbacks exposed to JS)
|
|
25
|
+
- Custom theming hooks beyond what the HubSpot dashboard chatflow already provides
|
|
26
|
+
|
|
27
|
+
PRs welcome.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- React Native 0.81+
|
|
32
|
+
- New Architecture enabled
|
|
33
|
+
- iOS 15+
|
|
34
|
+
- Android minSdk 26
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
yarn add react-native-hubspot-wrapper
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then install iOS dependencies:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
cd ios && pod install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
- iOS: include `Hubspot-Info.plist` in your app target.
|
|
51
|
+
- Android: include `android/app/src/main/assets/hubspot-info.json`.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import HubspotWrapper from 'react-native-hubspot-wrapper';
|
|
57
|
+
|
|
58
|
+
await HubspotWrapper.initialize();
|
|
59
|
+
await HubspotWrapper.setIdentity({ identityToken: 'token', email: 'user@example.com' });
|
|
60
|
+
await HubspotWrapper.setProperties([{ name: 'plan', value: 'pro' }]);
|
|
61
|
+
await HubspotWrapper.openChat('support');
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
- `initialize(): Promise<void>`
|
|
67
|
+
- `openChat(chatflow: string): Promise<void>`
|
|
68
|
+
- `setIdentity({ identityToken, email? }): Promise<void>`
|
|
69
|
+
- `setProperties(properties): Promise<void>`
|
|
70
|
+
- `clearUserData(): Promise<void>`
|
|
71
|
+
|
|
72
|
+
## iOS SDK source strategy
|
|
73
|
+
|
|
74
|
+
This package vendors HubSpot iOS SDK source files under `ios/HubspotMobileSDK`.
|
|
75
|
+
The files are intentionally committed to git for reproducible CocoaPods builds,
|
|
76
|
+
since React Native does not yet integrate Swift Package Manager natively.
|
|
77
|
+
|
|
78
|
+
Current vendored source metadata is tracked in `HUBSPOT_IOS_SDK_VERSION.json`.
|
|
79
|
+
The upstream `LICENSE.txt` is preserved alongside the vendored sources at
|
|
80
|
+
`ios/HubspotMobileSDK/LICENSE.txt`, in compliance with the MIT license HubSpot
|
|
81
|
+
ships their iOS SDK under.
|
|
82
|
+
|
|
83
|
+
When React Native adds first-class SwiftPM support, the vendored sources can be
|
|
84
|
+
removed and replaced with a `Package.swift` dependency without any consumer-facing
|
|
85
|
+
API changes.
|
|
86
|
+
|
|
87
|
+
## Updating vendored HubSpot iOS SDK
|
|
88
|
+
|
|
89
|
+
Use the helper script:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
yarn update:hubspot:ios
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Note: the update script also reapplies a small CocoaPods compatibility patch set
|
|
96
|
+
to HubSpot iOS sources (resource bundle + asset/localization access), so the
|
|
97
|
+
wrapper remains buildable outside of pure Swift Package Manager integration.
|
|
98
|
+
|
|
99
|
+
Optional: update to an explicit tag:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
yarn update:hubspot:ios 1.0.7
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
After updating:
|
|
106
|
+
|
|
107
|
+
1. run `cd ios && pod install` in the consuming app
|
|
108
|
+
2. run iOS/Android compile checks
|
|
109
|
+
3. commit updated `ios/HubspotMobileSDK` and `HUBSPOT_IOS_SDK_VERSION.json`
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
This wrapper is released under the [MIT license](./LICENSE).
|
|
114
|
+
|
|
115
|
+
The vendored HubSpot iOS SDK source under `ios/HubspotMobileSDK/` is also
|
|
116
|
+
MIT-licensed by HubSpot, Inc. — see `ios/HubspotMobileSDK/LICENSE.txt`.
|
|
117
|
+
|
|
118
|
+
The HubSpot Android SDK is not redistributed by this package; it is fetched
|
|
119
|
+
from Maven Central by the consuming app under HubSpot's terms.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ReactNativeHubspotWrapper"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.description = package["description"]
|
|
10
|
+
s.homepage = "https://github.com/marcinolek/react-native-hubspot-wrapper"
|
|
11
|
+
s.license = package["license"]
|
|
12
|
+
s.authors = "Marcin Olek"
|
|
13
|
+
s.platforms = { :ios => "15.0" }
|
|
14
|
+
s.source = { :path => "." }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
s.resource_bundles = {
|
|
19
|
+
"HubspotMobileSDKResources" => ["ios/HubspotMobileSDK/Resources/**/*"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
23
|
+
install_modules_dependencies(s)
|
|
24
|
+
else
|
|
25
|
+
s.dependency "React-Core"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.kotlin_version = "1.9.22"
|
|
3
|
+
repositories {
|
|
4
|
+
google()
|
|
5
|
+
mavenCentral()
|
|
6
|
+
}
|
|
7
|
+
dependencies {
|
|
8
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
apply plugin: "com.android.library"
|
|
13
|
+
apply plugin: "kotlin-android"
|
|
14
|
+
apply plugin: "com.facebook.react"
|
|
15
|
+
|
|
16
|
+
android {
|
|
17
|
+
compileSdk 34
|
|
18
|
+
namespace "com.marcinolek.reactnativehubspotwrapper"
|
|
19
|
+
|
|
20
|
+
defaultConfig {
|
|
21
|
+
minSdk 26
|
|
22
|
+
targetSdk 34
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sourceSets {
|
|
26
|
+
main.java.srcDirs += "src/main/kotlin"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
compileOptions {
|
|
30
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
31
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
32
|
+
}
|
|
33
|
+
kotlinOptions {
|
|
34
|
+
jvmTarget = "17"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
react {
|
|
39
|
+
jsRootDir = file("../")
|
|
40
|
+
libraryName = "HubspotWrapperSpec"
|
|
41
|
+
codegenJavaPackageName = "com.marcinolek.reactnativehubspotwrapper"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dependencies {
|
|
45
|
+
implementation "com.facebook.react:react-native:+"
|
|
46
|
+
implementation "com.hubspot.mobilechatsdk:mobile-chat-sdk-android:1.0.8"
|
|
47
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1"
|
|
48
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest package="com.marcinolek.reactnativehubspotwrapper" />
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package com.marcinolek.reactnativehubspotwrapper
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReadableArray
|
|
7
|
+
import com.hubspot.mobilesdk.HubspotManager
|
|
8
|
+
import com.hubspot.mobilesdk.HubspotWebActivity
|
|
9
|
+
import kotlinx.coroutines.CoroutineScope
|
|
10
|
+
import kotlinx.coroutines.Dispatchers
|
|
11
|
+
import kotlinx.coroutines.launch
|
|
12
|
+
|
|
13
|
+
class HubspotWrapperModule(reactContext: ReactApplicationContext) :
|
|
14
|
+
NativeHubspotWrapperSpec(reactContext) {
|
|
15
|
+
|
|
16
|
+
private val appContext = reactContext.applicationContext
|
|
17
|
+
private lateinit var hubspotManager: HubspotManager
|
|
18
|
+
|
|
19
|
+
override fun getName(): String = NAME
|
|
20
|
+
|
|
21
|
+
override fun initialize(promise: Promise) {
|
|
22
|
+
try {
|
|
23
|
+
hubspotManager = HubspotManager.getInstance(appContext)
|
|
24
|
+
hubspotManager.enableLogs()
|
|
25
|
+
hubspotManager.configure()
|
|
26
|
+
promise.resolve(null)
|
|
27
|
+
} catch (error: Exception) {
|
|
28
|
+
promise.reject("INIT_ERROR", "Failed to initialize HubSpot SDK", error)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override fun openChat(chatflow: String, promise: Promise) {
|
|
33
|
+
try {
|
|
34
|
+
val intent = Intent(appContext, HubspotWebActivity::class.java)
|
|
35
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
36
|
+
intent.putExtra("chatflow", chatflow)
|
|
37
|
+
appContext.startActivity(intent)
|
|
38
|
+
promise.resolve(null)
|
|
39
|
+
} catch (error: Exception) {
|
|
40
|
+
promise.reject("OPEN_CHAT_ERROR", "Failed to open HubSpot chat", error)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun setIdentity(identityToken: String, email: String?, promise: Promise) {
|
|
45
|
+
try {
|
|
46
|
+
hubspotManager.setUserIdentity(identityToken, email ?: "")
|
|
47
|
+
promise.resolve(null)
|
|
48
|
+
} catch (error: Exception) {
|
|
49
|
+
promise.reject("IDENTITY_ERROR", "Failed to set HubSpot identity", error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun setProperties(properties: ReadableArray, promise: Promise) {
|
|
54
|
+
try {
|
|
55
|
+
val mapped = mutableMapOf<String, String>()
|
|
56
|
+
for (i in 0 until properties.size()) {
|
|
57
|
+
val item = properties.getMap(i) ?: continue
|
|
58
|
+
val name = item.getString("name") ?: continue
|
|
59
|
+
val value = item.getString("value") ?: ""
|
|
60
|
+
mapped[name] = value
|
|
61
|
+
}
|
|
62
|
+
hubspotManager.setChatProperties(mapped)
|
|
63
|
+
promise.resolve(null)
|
|
64
|
+
} catch (error: Exception) {
|
|
65
|
+
promise.reject("SET_PROPERTIES_ERROR", "Failed to set HubSpot chat properties", error)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun clearUserData(promise: Promise) {
|
|
70
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
71
|
+
try {
|
|
72
|
+
hubspotManager.logout()
|
|
73
|
+
promise.resolve(null)
|
|
74
|
+
} catch (error: Exception) {
|
|
75
|
+
promise.reject("CLEAR_USER_DATA_ERROR", "Failed to clear HubSpot user data", error)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
companion object {
|
|
81
|
+
const val NAME = "NativeHubspotWrapper"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.marcinolek.reactnativehubspotwrapper
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.TurboReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class HubspotWrapperPackage : TurboReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
11
|
+
return if (name == HubspotWrapperModule.NAME) {
|
|
12
|
+
HubspotWrapperModule(reactContext)
|
|
13
|
+
} else {
|
|
14
|
+
null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
19
|
+
return ReactModuleInfoProvider {
|
|
20
|
+
mapOf(
|
|
21
|
+
HubspotWrapperModule.NAME to ReactModuleInfo(
|
|
22
|
+
HubspotWrapperModule.NAME,
|
|
23
|
+
HubspotWrapperModule.NAME,
|
|
24
|
+
false,
|
|
25
|
+
false,
|
|
26
|
+
false,
|
|
27
|
+
true
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// APIModels.swift
|
|
2
|
+
// Hubspot Mobile SDK
|
|
3
|
+
//
|
|
4
|
+
// Copyright © 2024 Hubspot, Inc.
|
|
5
|
+
|
|
6
|
+
import Foundation
|
|
7
|
+
|
|
8
|
+
/// This is the body of the api request for sending chat properties, to help with JSON encoding.
|
|
9
|
+
struct ChatPropertyMetadataRequest: Encodable {
|
|
10
|
+
/// The visitor id token , provided by the app itself. This is the token obtained from app backend from hubspot api
|
|
11
|
+
let visitorToken: String?
|
|
12
|
+
|
|
13
|
+
/// The email provided with the visitor token itself.
|
|
14
|
+
let email: String?
|
|
15
|
+
|
|
16
|
+
/// dictionary of arbitary key and values to send to the api - see ``ChatPropertyKey``
|
|
17
|
+
/// - seeAlso: ``ChatPropertyKey``
|
|
18
|
+
let metadata: [String: String]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Model used for serialisation when creating a visitor token
|
|
22
|
+
struct CreateVisitorTokenRequest: Codable {
|
|
23
|
+
let email: String
|
|
24
|
+
let firstName: String
|
|
25
|
+
let lastName: String
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Model for serialising post body of adding a device token
|
|
29
|
+
struct StoreDeviceTokenRequest: Encodable {
|
|
30
|
+
let devicePushToken: String
|
|
31
|
+
let platform = "ios"
|
|
32
|
+
// Do we need user id here?
|
|
33
|
+
|
|
34
|
+
init(devicePushToken: Data) {
|
|
35
|
+
self.devicePushToken = devicePushToken.toHexString()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Model used for deserialising the create visitor token response
|
|
40
|
+
struct CreateVisitorTokenResponse: Codable {
|
|
41
|
+
let token: String
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
extension Data {
|
|
45
|
+
/// Used to encode push tokens
|
|
46
|
+
/// - Returns: The data in hex format, lowercase
|
|
47
|
+
func toHexString() -> String {
|
|
48
|
+
return map { String(format: "%02hhx", $0) }.joined()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// HubspotAPI.swift
|
|
2
|
+
// Hubspot Mobile SDK
|
|
3
|
+
//
|
|
4
|
+
// Copyright © 2024 Hubspot, Inc.
|
|
5
|
+
|
|
6
|
+
import Foundation
|
|
7
|
+
import OSLog
|
|
8
|
+
|
|
9
|
+
/// Not public class yet, as its not known if we need to expose API directly to app.
|
|
10
|
+
final class HubspotAPI: Sendable {
|
|
11
|
+
let logger: Logger
|
|
12
|
+
|
|
13
|
+
init(logger: Logger) {
|
|
14
|
+
self.logger = logger
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Errors relating to API requests and reponses that the SDK may make.
|
|
18
|
+
enum APIError: Error {
|
|
19
|
+
/// Unable to form a request - likely due to poor configuration or formatting
|
|
20
|
+
case requestError
|
|
21
|
+
|
|
22
|
+
/// Response couldn't be handled as expected, perhaps due to decoding error or similar. Original error included.
|
|
23
|
+
case responseError(Error)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private let jsonEncoder: JSONEncoder = .init()
|
|
27
|
+
private let jsonDecoder: JSONDecoder = .init()
|
|
28
|
+
|
|
29
|
+
private let urlSession = URLSession(configuration: .default)
|
|
30
|
+
|
|
31
|
+
func sendDeviceToken(hublet: Hublet, token: Data, portalId: String) async throws {
|
|
32
|
+
// POST
|
|
33
|
+
let apiUrl = hublet.apiURL.appendingPathComponent("livechat-public/v1/mobile-sdk/device-token")
|
|
34
|
+
|
|
35
|
+
guard var components = URLComponents(url: apiUrl, resolvingAgainstBaseURL: false) else {
|
|
36
|
+
throw APIError.requestError
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
components.queryItems = [URLQueryItem(name: "portalId", value: portalId)]
|
|
40
|
+
|
|
41
|
+
guard let url = components.url else {
|
|
42
|
+
throw APIError.requestError
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var request = URLRequest(url: url)
|
|
46
|
+
request.httpMethod = "POST"
|
|
47
|
+
|
|
48
|
+
let postBodyModel = StoreDeviceTokenRequest(devicePushToken: token)
|
|
49
|
+
let requestData = try jsonEncoder.encode(postBodyModel)
|
|
50
|
+
|
|
51
|
+
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
52
|
+
request.httpBody = requestData
|
|
53
|
+
|
|
54
|
+
let (data, _) = try await urlSession.data(for: request)
|
|
55
|
+
|
|
56
|
+
#if DEBUG
|
|
57
|
+
// Right now , we aren't using the reponse, but just logging it temporiarly to see if we get a response - I expect this to change once the api gets integrated in server, then we can either use the response or ignore it.
|
|
58
|
+
let bodyString = String(data: data, encoding: .utf8)
|
|
59
|
+
logger.trace("Response from sending token: \(bodyString ?? "<EMPTY>")")
|
|
60
|
+
#endif
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func deleteDeviceToken(hublet: Hublet, token: Data, portalId: String) async throws {
|
|
64
|
+
// DELETE
|
|
65
|
+
let apiUrl = hublet.apiURL.appendingPathComponent("livechat-public/v1/mobile-sdk/device-token/\(token.toHexString())")
|
|
66
|
+
|
|
67
|
+
guard var components = URLComponents(url: apiUrl, resolvingAgainstBaseURL: false) else {
|
|
68
|
+
throw APIError.requestError
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
components.queryItems = [URLQueryItem(name: "portalId", value: portalId)]
|
|
72
|
+
|
|
73
|
+
guard let url = components.url else {
|
|
74
|
+
throw APIError.requestError
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
var request = URLRequest(url: url)
|
|
78
|
+
request.httpMethod = "DELETE"
|
|
79
|
+
|
|
80
|
+
let (data, _) = try await urlSession.data(for: request)
|
|
81
|
+
|
|
82
|
+
#if DEBUG
|
|
83
|
+
// Right now , we aren't using the reponse, but just logging it temporiarly to see if we get a response - I expect this to change once the api gets integrated in server, then we can either use the response or ignore it.
|
|
84
|
+
let bodyString = String(data: data, encoding: .utf8)
|
|
85
|
+
logger.trace("Response from deleting token: \(bodyString ?? "<EMPTY>")")
|
|
86
|
+
#endif
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Post chat properties for a specific thread id to the api.
|
|
90
|
+
/// - Parameters:
|
|
91
|
+
/// - hublet: destination hublet
|
|
92
|
+
/// - properties: The collection of properties to post.
|
|
93
|
+
/// - visitorIdToken: The token set by the app to identify user. Optional.
|
|
94
|
+
/// - threadId: The thread id read from the chat view / javascript bridge that identifies the current open thread
|
|
95
|
+
/// - portalId: Account portal id
|
|
96
|
+
func sendChatProperties(hublet: Hublet, properties: [String: String], visitorIdToken: String?, email: String?, threadId: String, portalId: String) async throws {
|
|
97
|
+
let urlProperties = ["portalId": portalId, "threadId": threadId]
|
|
98
|
+
|
|
99
|
+
let apiUrl = hublet.apiURL.appendingPathComponent("livechat-public/v1/mobile-sdk/metadata")
|
|
100
|
+
guard var urlBuilder = URLComponents(url: apiUrl, resolvingAgainstBaseURL: false) else {
|
|
101
|
+
throw APIError.requestError
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
urlBuilder.queryItems = urlProperties.map { key, value in URLQueryItem(name: key, value: value) }
|
|
105
|
+
|
|
106
|
+
guard let url = urlBuilder.url else {
|
|
107
|
+
throw APIError.requestError
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let requestModel = ChatPropertyMetadataRequest(visitorToken: visitorIdToken, email: email, metadata: properties)
|
|
111
|
+
let requestData = try jsonEncoder.encode(requestModel)
|
|
112
|
+
|
|
113
|
+
var request = URLRequest(url: url)
|
|
114
|
+
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
115
|
+
request.httpMethod = "POST"
|
|
116
|
+
|
|
117
|
+
request.httpBody = requestData
|
|
118
|
+
let (_, response) = try await urlSession.data(for: request)
|
|
119
|
+
|
|
120
|
+
if let httpResponse = response as? HTTPURLResponse {
|
|
121
|
+
// We aren't expecting any content as a response, just that it succeeded - hopefully the try await above is sufficient.
|
|
122
|
+
// Logging the code in debug builds just to confirm for now
|
|
123
|
+
#if DEBUG
|
|
124
|
+
if case 200 ..< 300 = httpResponse.statusCode {
|
|
125
|
+
logger.trace("Sending metadata was a 2XX response: \(httpResponse.statusCode)")
|
|
126
|
+
}
|
|
127
|
+
#endif
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Convenience for creating a visitor identity token using the given details, for situations where server infrastructure isn't available.
|
|
132
|
+
///
|
|
133
|
+
/// - WARNING: Embedding access token for your product in app is not recommended - This was originally for demo purposes, and may be removed. Strongly consider creating a token as part of app server infrastructure instead.
|
|
134
|
+
///
|
|
135
|
+
/// - Parameters:
|
|
136
|
+
/// - hublet: destination hublet
|
|
137
|
+
/// - accessToken: The access token for your application, as returned by the Hubspot dashboard
|
|
138
|
+
/// - email: the email of the user
|
|
139
|
+
/// - firstName: users first name
|
|
140
|
+
/// - lastName: users last name
|
|
141
|
+
/// - Returns: The generated JWT token
|
|
142
|
+
func createVisitorToken(hublet: Hublet, accessToken: String, email: String, firstName: String, lastName: String) async throws -> String {
|
|
143
|
+
// Later, if we have lots of requests we can refactor this to have common base path
|
|
144
|
+
let url = hublet.apiURL.appendingPathComponent("conversations/v3/visitor-identification/tokens/create")
|
|
145
|
+
|
|
146
|
+
let requestModel = CreateVisitorTokenRequest(email: email, firstName: firstName, lastName: lastName)
|
|
147
|
+
let requestData = try jsonEncoder.encode(requestModel)
|
|
148
|
+
|
|
149
|
+
var request = URLRequest(url: url)
|
|
150
|
+
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
|
151
|
+
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
152
|
+
request.httpMethod = "POST"
|
|
153
|
+
|
|
154
|
+
request.httpBody = requestData
|
|
155
|
+
|
|
156
|
+
let (data, response) = try await urlSession.data(for: request)
|
|
157
|
+
|
|
158
|
+
do {
|
|
159
|
+
let responseModel = try jsonDecoder.decode(CreateVisitorTokenResponse.self, from: data)
|
|
160
|
+
return responseModel.token
|
|
161
|
+
} catch {
|
|
162
|
+
let bodyString = String(data: data, encoding: .utf8)
|
|
163
|
+
/// Catching just to log, and then re-throw it wrapped
|
|
164
|
+
logger.error("Failed to decode visitor token model: \(error) - response was \(response), body contents: \(bodyString ?? "not set")")
|
|
165
|
+
throw APIError.responseError(error)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// DeviceTokenSyncState.swift
|
|
2
|
+
// Hubspot Mobile SDK
|
|
3
|
+
//
|
|
4
|
+
// Copyright © 2024 Hubspot, Inc.
|
|
5
|
+
|
|
6
|
+
import Foundation
|
|
7
|
+
|
|
8
|
+
/// Enum to help track if we have posted our device token, and when
|
|
9
|
+
enum DeviceTokenSyncState: Equatable {
|
|
10
|
+
case notSent
|
|
11
|
+
case sending(Date)
|
|
12
|
+
case sent(Date)
|
|
13
|
+
}
|