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.
Files changed (36) hide show
  1. package/HUBSPOT_IOS_SDK_VERSION.json +5 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.md +19 -0
  4. package/README.md +119 -0
  5. package/ReactNativeHubspotWrapper.podspec +27 -0
  6. package/android/build.gradle +49 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/java/com/reactnativehubspotwrapper/HubspotWrapperModule.kt +83 -0
  9. package/android/src/main/java/com/reactnativehubspotwrapper/HubspotWrapperPackage.kt +32 -0
  10. package/ios/HubspotMobileSDK/API/APIModels.swift +50 -0
  11. package/ios/HubspotMobileSDK/API/HubspotAPI.swift +168 -0
  12. package/ios/HubspotMobileSDK/DeviceTokenSyncState.swift +13 -0
  13. package/ios/HubspotMobileSDK/HubspotConfig.swift +145 -0
  14. package/ios/HubspotMobileSDK/HubspotManager+Notifications.swift +178 -0
  15. package/ios/HubspotMobileSDK/HubspotManager+Properties.swift +53 -0
  16. package/ios/HubspotMobileSDK/HubspotManager.swift +548 -0
  17. package/ios/HubspotMobileSDK/HubspotMobileSDK.swift +7 -0
  18. package/ios/HubspotMobileSDK/HubspotUserProperties.swift +115 -0
  19. package/ios/HubspotMobileSDK/LICENSE.txt +19 -0
  20. package/ios/HubspotMobileSDK/PushNotificationChatData.swift +63 -0
  21. package/ios/HubspotMobileSDK/Resources/Images.xcassets/Contents.json +6 -0
  22. package/ios/HubspotMobileSDK/Resources/Images.xcassets/GenericChatIcon.imageset/Contents.json +16 -0
  23. package/ios/HubspotMobileSDK/Resources/Images.xcassets/GenericChatIcon.imageset/chat-open-svg.svg +1 -0
  24. package/ios/HubspotMobileSDK/Resources/Localizable.xcstrings +28 -0
  25. package/ios/HubspotMobileSDK/Resources/PrivacyInfo.xcprivacy +62 -0
  26. package/ios/HubspotMobileSDK/Views/Buttons/FloatingActionButton.swift +126 -0
  27. package/ios/HubspotMobileSDK/Views/Buttons/TextChatButtonChatButton.swift +78 -0
  28. package/ios/HubspotMobileSDK/Views/ChatView/HubspotChatView.swift +612 -0
  29. package/ios/HubspotWrapperImpl.swift +108 -0
  30. package/ios/RNHubspotWrapper.h +9 -0
  31. package/ios/RNHubspotWrapper.mm +66 -0
  32. package/package.json +55 -0
  33. package/react-native.config.js +11 -0
  34. package/scripts/update-hubspot-ios-sdk.sh +142 -0
  35. package/src/index.ts +41 -0
  36. package/src/specs/NativeHubspotWrapper.ts +17 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "sourceRepository": "https://github.com/HubSpot/mobile-chat-sdk-ios.git",
3
+ "tag": "1.0.7",
4
+ "commit": "be33ea03c2f14d9ae47079128edf1f3c7c11d6a8"
5
+ }
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
+ }