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
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
// HubspotManager.swift
|
|
2
|
+
// Hubspot Mobile SDK
|
|
3
|
+
//
|
|
4
|
+
// Copyright © 2024 Hubspot, Inc.
|
|
5
|
+
|
|
6
|
+
import Combine
|
|
7
|
+
import Foundation
|
|
8
|
+
import OSLog
|
|
9
|
+
import SwiftUI
|
|
10
|
+
@preconcurrency import UserNotifications
|
|
11
|
+
import WebKit
|
|
12
|
+
|
|
13
|
+
/// Logger is created in multiple places, so this is a helper for that to avoid repeating values
|
|
14
|
+
private func createDefaultHubspotLogger() -> Logger {
|
|
15
|
+
Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.hubspot.mobilesdk", category: "HubspotSDK")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// The main interface for the Hubspot mobile SDK.
|
|
19
|
+
///
|
|
20
|
+
/// Call ``configure()-swift.type.method`` before using. Chat sessions can be started & shown using ``HubspotChatView``
|
|
21
|
+
///
|
|
22
|
+
/// Use ``setUserIdentity(identityToken:email:)`` to optionally identify users with server side generated tokens.
|
|
23
|
+
///
|
|
24
|
+
/// Use ``setChatProperties(data:)`` to include additional data , including custom key value pairs that are useful during chat sessions.
|
|
25
|
+
///
|
|
26
|
+
/// For more setup instructions, see <doc:GettingStarted>
|
|
27
|
+
///
|
|
28
|
+
///
|
|
29
|
+
@MainActor
|
|
30
|
+
public class HubspotManager: NSObject, ObservableObject {
|
|
31
|
+
|
|
32
|
+
/// These are the cookies HubSpot sets to manage identity for chat sessions; we want to remove them to allow fresh identities to be created on future chat sessions
|
|
33
|
+
/// More info on cookies that might be set are here: https://knowledge.hubspot.com/privacy-and-consent/what-cookies-does-hubspot-set-in-a-visitor-s-browser
|
|
34
|
+
private let cookiesToDeleteWhenClearingData = ["hubspotutk", "messagesUtk"]
|
|
35
|
+
|
|
36
|
+
/// Shared instance that can be used app wide, instead of creating an managing own instance.
|
|
37
|
+
/// If not using this instance, and instead managing your own instance, make sure to pass your instance as an argument to the ``HubspotChatView`` or other components.
|
|
38
|
+
public static let shared = HubspotManager()
|
|
39
|
+
|
|
40
|
+
/// The hublet to use, if configured
|
|
41
|
+
public private(set) var hublet: String?
|
|
42
|
+
|
|
43
|
+
/// The portalId to use, if configured
|
|
44
|
+
public private(set) var portalId: String?
|
|
45
|
+
|
|
46
|
+
/// The default chat flow, if configured
|
|
47
|
+
public private(set) var defaultChatFlow: String?
|
|
48
|
+
|
|
49
|
+
/// the currently configured environment
|
|
50
|
+
public private(set) var environment: HubspotEnvironment = .production
|
|
51
|
+
|
|
52
|
+
/// The identity token currently set for the user
|
|
53
|
+
/// - seeAlso: ``setUserIdentity(identityToken:email:)``
|
|
54
|
+
public private(set) var userIdentityToken: String?
|
|
55
|
+
|
|
56
|
+
/// The email address currently set for the user
|
|
57
|
+
/// - seeAlso: ``setUserIdentity(identityToken:email:)``
|
|
58
|
+
public private(set) var userEmailAddress: String?
|
|
59
|
+
|
|
60
|
+
/// The push token - this should be set by the app whenever it has access to the token.
|
|
61
|
+
/// - seeAlso: ``setPushToken(apnsPushToken:)``
|
|
62
|
+
public private(set) var pushToken: Data?
|
|
63
|
+
|
|
64
|
+
/// We might not be in a position to send the token immediately, so we need to track if we need to send it later, after configuration
|
|
65
|
+
private var pushTokenSyncState: DeviceTokenSyncState = .notSent
|
|
66
|
+
private var sendPushTokenTask: Task<Void, Never>?
|
|
67
|
+
|
|
68
|
+
/// This is the collection of user properties that are sent to Hubspot on opening chat sessions. set with ``setChatProperties(data:)``
|
|
69
|
+
/// - seeAlso: ChatPropertyKey
|
|
70
|
+
public private(set) var chatProperties: [String: String] = [:]
|
|
71
|
+
|
|
72
|
+
/// Callback triggered when user opens a hubspot message. Only triggered when manager is acting as the UNNotificationDelegate, or notifications are forwarded to the ``userNotificationCenter(_:didReceive:withCompletionHandler:)`` method on this manager instance.
|
|
73
|
+
public var newMessageCallback: (PushNotificationChatData) -> Void = { _ in }
|
|
74
|
+
|
|
75
|
+
/// Publisher triggered when user opens a hubspot message. Only triggered when manager is acting as the UNNotificationDelegate, or notifications are forwarded to the ``userNotificationCenter(_:didReceive:withCompletionHandler:)`` method on this manager instance.
|
|
76
|
+
public private(set) var newMessage: PassthroughSubject<PushNotificationChatData, Never> = PassthroughSubject()
|
|
77
|
+
|
|
78
|
+
/// The logger used by the SDK. to disable logging set to the disabled OSLog with `Logger(.disabled)`, or set a custom Logger with a preferred subsystem and category
|
|
79
|
+
public var logger = createDefaultHubspotLogger() {
|
|
80
|
+
didSet {
|
|
81
|
+
// Replace API with new instance with new logger
|
|
82
|
+
api = HubspotAPI(logger: logger)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private var hubletModel: Hublet? {
|
|
87
|
+
guard let hublet
|
|
88
|
+
else {
|
|
89
|
+
return nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return Hublet(id: hublet, environment: environment)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Record if we turned on battery monitoring. If we didn't turn it on , we might not want to turn it off again.
|
|
96
|
+
var didWeEnableBatterMonitoring: Bool = false
|
|
97
|
+
/// Record if we turned on origentation monitoring. If we didn't turn it on , we might not want to turn it off again.
|
|
98
|
+
var didWeEnableOrientationMonitoring: Bool = false
|
|
99
|
+
|
|
100
|
+
private var api: HubspotAPI
|
|
101
|
+
|
|
102
|
+
/// Use the provided config values, applied to the shared instance ``shared``
|
|
103
|
+
/// - Parameters:
|
|
104
|
+
/// - portalId: Your portal id - you can find it from your hubspot account page
|
|
105
|
+
/// - hublet: Hublet name , typically "na1" or "eu1"
|
|
106
|
+
/// - defaultChatFlow: The default chat flow to use if none is specified per chat view
|
|
107
|
+
/// - environment: the environment to use
|
|
108
|
+
public static func configure(
|
|
109
|
+
portalId: String,
|
|
110
|
+
hublet: String,
|
|
111
|
+
defaultChatFlow: String?,
|
|
112
|
+
environment: HubspotEnvironment = .production
|
|
113
|
+
) {
|
|
114
|
+
shared.configure(
|
|
115
|
+
portalId: portalId,
|
|
116
|
+
hublet: hublet,
|
|
117
|
+
defaultChatFlow: defaultChatFlow,
|
|
118
|
+
environment: environment
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Load SDK configuration from bundled config file. Note this only applies to the shared instance ``shared`` , if you intend to create a new instance of `HubspotManager`, you should use the non static version on that instance , ``configure()-swift.method``
|
|
123
|
+
///
|
|
124
|
+
/// throws ``HubspotConfigError`` if config file isnt as expected
|
|
125
|
+
public static func configure() throws {
|
|
126
|
+
try shared.configure()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Create unconfigured SDK instance - not currently set public, use shared instance for now.
|
|
130
|
+
override init() {
|
|
131
|
+
api = HubspotAPI(logger: logger)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Configure this SDK instance with the bundled `Hubspot-Info.plist` config file from the main bundle.
|
|
135
|
+
/// - throws: `HubspotConfigError.missingConfiguration` thrown if config file cannot be found in the bundle, or if it contains missing required items.
|
|
136
|
+
public func configure() throws {
|
|
137
|
+
guard let plistUrl = Bundle.main.url(forResource: HubspotConfig.defaultConfigFileName, withExtension: nil) else {
|
|
138
|
+
throw HubspotConfigError.missingConfiguration
|
|
139
|
+
}
|
|
140
|
+
let plistData = try Data(contentsOf: plistUrl)
|
|
141
|
+
let decoder = PropertyListDecoder()
|
|
142
|
+
|
|
143
|
+
do {
|
|
144
|
+
let config = try decoder.decode(HubspotConfig.self, from: plistData)
|
|
145
|
+
logger.trace("Loaded config with portal id of \(config.portalId)")
|
|
146
|
+
hublet = config.hublet
|
|
147
|
+
portalId = config.portalId
|
|
148
|
+
environment = config.environment
|
|
149
|
+
defaultChatFlow = config.defaultChatFlow
|
|
150
|
+
objectWillChange.send()
|
|
151
|
+
|
|
152
|
+
sendPushTokenIfNeeded()
|
|
153
|
+
|
|
154
|
+
} catch {
|
|
155
|
+
logger.error("Error decoding plist - check all expected keys exist: \(error)")
|
|
156
|
+
throw HubspotConfigError.missingConfiguration
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Configure SDK with given values
|
|
161
|
+
/// - Parameters:
|
|
162
|
+
/// - portalId: Your portal id - you can find it from your hubspot account page
|
|
163
|
+
/// - hublet: Hublet name , typically "na1" or "eu1"
|
|
164
|
+
/// - defaultChatFlow: chat flow to use when none is specified when creating a chat. For example: sales
|
|
165
|
+
/// - environment: the environment to use
|
|
166
|
+
func configure(
|
|
167
|
+
portalId: String,
|
|
168
|
+
hublet: String,
|
|
169
|
+
defaultChatFlow: String?,
|
|
170
|
+
environment: HubspotEnvironment = .production
|
|
171
|
+
) {
|
|
172
|
+
self.portalId = portalId
|
|
173
|
+
self.hublet = hublet
|
|
174
|
+
self.environment = environment
|
|
175
|
+
self.defaultChatFlow = defaultChatFlow
|
|
176
|
+
|
|
177
|
+
objectWillChange.send()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Convenience to set the logger to the disabled logger
|
|
181
|
+
public func disableLogging() {
|
|
182
|
+
logger = Logger(.disabled)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Re-configures ``logger`` with the default logger config - if you want a specific logger category, configure ``logger`` directly instead
|
|
186
|
+
public func enableLogging() {
|
|
187
|
+
logger = createDefaultHubspotLogger()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Set the push token for the app. Recommend calling this each app launch when push feature is enabled.
|
|
191
|
+
/// - Parameter apnsPushToken: The data token provided by iOS via didRegisterForRemoteNotificationsWithDeviceToken
|
|
192
|
+
public func setPushToken(apnsPushToken: Data) {
|
|
193
|
+
/// Only reset our state when it actually changes
|
|
194
|
+
if pushToken != apnsPushToken {
|
|
195
|
+
pushTokenSyncState = .notSent
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pushToken = apnsPushToken
|
|
199
|
+
sendPushTokenIfNeeded()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Sends the token if not already sent - only sends data when we have the token and its not being sent recently
|
|
203
|
+
private func sendPushTokenIfNeeded() {
|
|
204
|
+
guard let pushToken,
|
|
205
|
+
let portalId
|
|
206
|
+
else {
|
|
207
|
+
// Not enough info, can't send yet
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
sendPushTokenTask = Task {
|
|
212
|
+
let shouldSendToken: Bool
|
|
213
|
+
|
|
214
|
+
switch self.pushTokenSyncState {
|
|
215
|
+
case .notSent:
|
|
216
|
+
shouldSendToken = true
|
|
217
|
+
|
|
218
|
+
case .sending(let lastActionDate):
|
|
219
|
+
let interval = abs(lastActionDate.timeIntervalSinceNow)
|
|
220
|
+
|
|
221
|
+
// If we have been 'sending' for more than a minute, clear and try again
|
|
222
|
+
if interval > 60 {
|
|
223
|
+
shouldSendToken = true
|
|
224
|
+
self.pushTokenSyncState = .notSent
|
|
225
|
+
} else {
|
|
226
|
+
shouldSendToken = false
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case .sent(let lastActionDate):
|
|
230
|
+
// Ignore new attempt to send if its within a brief window
|
|
231
|
+
let interval = abs(lastActionDate.timeIntervalSinceNow)
|
|
232
|
+
|
|
233
|
+
// Arbitary 20 second time to prevent double triggers in the case of repeated registration, setting identity at the same time, etc
|
|
234
|
+
shouldSendToken = interval > 20
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
guard shouldSendToken else {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let previousState = self.pushTokenSyncState
|
|
242
|
+
self.pushTokenSyncState = .sending(.now)
|
|
243
|
+
|
|
244
|
+
do {
|
|
245
|
+
guard let hubletModel else {
|
|
246
|
+
throw HubspotConfigError.missingConfiguration
|
|
247
|
+
}
|
|
248
|
+
try await api.sendDeviceToken(hublet: hubletModel, token: pushToken, portalId: portalId)
|
|
249
|
+
|
|
250
|
+
if !Task.isCancelled {
|
|
251
|
+
self.pushTokenSyncState = .sent(.now)
|
|
252
|
+
} else {
|
|
253
|
+
self.pushTokenSyncState = previousState
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
logger.error("Error registering push token with API: \(error)")
|
|
257
|
+
if case .sending = previousState {
|
|
258
|
+
// We shouldn't have had a previous state of sending ...
|
|
259
|
+
self.pushTokenSyncState = .notSent
|
|
260
|
+
} else {
|
|
261
|
+
// Leave the value of sync state as it was, either with an older date or not sent yet
|
|
262
|
+
self.pushTokenSyncState = previousState
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Set the user id obtained from the [Visitor Identification API](https://developers.hubspot.com/docs/api/conversation/visitor-identification) , along with the users email address. These will be included when starting a chat session to identify the user. Its important to set these before starting a chat session, as they are needed during chat initialisation.
|
|
269
|
+
///
|
|
270
|
+
/// Object will change is triggered after setting values, for anything that may be observing this manager with Combine or SwiftUI state
|
|
271
|
+
///
|
|
272
|
+
/// Note: These values are only stored in memory and aren't persisted. Set them on each app launch or when changing user autentication status. This token has a short expiry, and should be re-set periodically.
|
|
273
|
+
///
|
|
274
|
+
/// - Parameters:
|
|
275
|
+
/// - token: The token from the identity api. Must not be empty.
|
|
276
|
+
/// - email: The users email address, that matches the token. Must not be empty
|
|
277
|
+
public func setUserIdentity(identityToken: String, email: String) {
|
|
278
|
+
guard !identityToken.isEmpty, !email.isEmpty else {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
userIdentityToken = identityToken
|
|
282
|
+
userEmailAddress = email
|
|
283
|
+
objectWillChange.send()
|
|
284
|
+
|
|
285
|
+
sendPushTokenIfNeeded()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Set a string key and value collection to be associate with any chat opened.
|
|
289
|
+
///
|
|
290
|
+
/// Set optional and custom chat properies to send to hubspot on opening chat. These can be set at any time prior to starting a chat session.
|
|
291
|
+
/// The data included here is sent to the Hubspot API only when opening a chat - if no chat is started the data is not sent anywere.
|
|
292
|
+
///
|
|
293
|
+
/// You can use any key value, including custom keys, whatever makes sense your your application and use of chat for support or troubleshooting.
|
|
294
|
+
///
|
|
295
|
+
/// For common, optional keys and values to use, for example to specify user location or permissions , see ``ChatPropertyKey``
|
|
296
|
+
///
|
|
297
|
+
/// Avoid including personal, private information in property values, and only include data that you have permission from the user to use.
|
|
298
|
+
///
|
|
299
|
+
/// The data passed here is combined with some automatic properties determined at run time - see the descriptions of the keys in ``ChatPropertyKey`` for information on automatically set values
|
|
300
|
+
///
|
|
301
|
+
/// An example of setting a mix of pre-defined properties, and custom properties
|
|
302
|
+
/// ```
|
|
303
|
+
/// var properties: [String: String] = [
|
|
304
|
+
/// ChatPropertyKey.cameraPermissions.rawValue: self.checkCameraPermissions(),
|
|
305
|
+
/// "myapp-install-id": appUniqueId,
|
|
306
|
+
/// "subscription-tier": "premium"
|
|
307
|
+
/// ]
|
|
308
|
+
/// HubspotManager.shared.setChatProperties(data: properties)
|
|
309
|
+
/// ```
|
|
310
|
+
///
|
|
311
|
+
/// > Info: These properties are only retained in memory, and not persisted. Set preferred values at least once per app launch. They can be also replaced at any time by calling ``setChatProperties(data:)`` again.
|
|
312
|
+
///
|
|
313
|
+
/// - seeAlso: ``ChatPropertyKey``
|
|
314
|
+
public func setChatProperties(data: [String: String]) {
|
|
315
|
+
chatProperties = data
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// Gathers together any user provided properties, as well as automatic properties into one collection. This will be the data included in API call
|
|
319
|
+
func finalizeChatProperties() async -> [String: String] {
|
|
320
|
+
// Start with developer provided ones, if any
|
|
321
|
+
var properties = chatProperties
|
|
322
|
+
|
|
323
|
+
let deviceModel = deviceModel()
|
|
324
|
+
|
|
325
|
+
if !deviceModel.isEmpty {
|
|
326
|
+
properties[ChatPropertyKey.deviceModel.rawValue] = deviceModel
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if let pushToken {
|
|
330
|
+
let encodedPushToken = pushToken.toHexString()
|
|
331
|
+
properties[ChatPropertyKey.pushToken.rawValue] = encodedPushToken
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let notificationSettings = await UNUserNotificationCenter.current().notificationSettings()
|
|
335
|
+
|
|
336
|
+
if notificationSettings.alertSetting == .enabled || notificationSettings.notificationCenterSetting == .enabled {
|
|
337
|
+
properties[ChatPropertyKey.notificationPermissions.rawValue] = "true"
|
|
338
|
+
} else {
|
|
339
|
+
properties[ChatPropertyKey.notificationPermissions.rawValue] = "false"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
properties[ChatPropertyKey.operatingSystemVersion.rawValue] = "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)"
|
|
343
|
+
|
|
344
|
+
if let infoDict = Bundle.main.infoDictionary,
|
|
345
|
+
let shortVersion = infoDict["CFBundleShortVersionString"],
|
|
346
|
+
let buildVersion = infoDict["CFBundleVersion"]
|
|
347
|
+
{
|
|
348
|
+
properties[ChatPropertyKey.appVersion.rawValue] = "\(shortVersion).\(buildVersion)"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let screenBounds = UIScreen.main.bounds
|
|
352
|
+
let screenWidth = screenBounds.width
|
|
353
|
+
let screenHeight = screenBounds.height
|
|
354
|
+
let scale = UIScreen.main.scale
|
|
355
|
+
|
|
356
|
+
properties[ChatPropertyKey.screenSize.rawValue] = "\(Int(screenWidth))x\(Int(screenHeight))"
|
|
357
|
+
properties[ChatPropertyKey.screenResolution.rawValue] = "\(Int(screenWidth * scale))x\(Int(screenHeight * scale))"
|
|
358
|
+
properties[ChatPropertyKey.deviceOrientation.rawValue] = UIDevice.current.orientation.hubspotApiValue
|
|
359
|
+
|
|
360
|
+
// ignore when battery is reported as -1, or some negative to indicate its invalid
|
|
361
|
+
if UIDevice.current.batteryLevel >= 0 {
|
|
362
|
+
let batteryLevelRounded = Int((UIDevice.current.batteryLevel * 100).rounded())
|
|
363
|
+
properties[ChatPropertyKey.batteryLevel.rawValue] = String(batteryLevelRounded)
|
|
364
|
+
}
|
|
365
|
+
properties[ChatPropertyKey.batteryState.rawValue] = UIDevice.current.batteryState.hubspotApiValue
|
|
366
|
+
|
|
367
|
+
// Platform is just fixed, no point in trying to detect it at runtime
|
|
368
|
+
properties[ChatPropertyKey.platform.rawValue] = "ios"
|
|
369
|
+
|
|
370
|
+
return properties
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// Delete all user specific data like identity tokens , email address, custom chat properties etc from any in memory or local stores.
|
|
374
|
+
/// Note: does not remove any property or user data from Hubspot remotely, except for attempting to remove the push token from the current user.
|
|
375
|
+
public func clearUserData() {
|
|
376
|
+
sendPushTokenTask?.cancel()
|
|
377
|
+
|
|
378
|
+
/// First, remove the push token, if we can
|
|
379
|
+
if let pushToken, let portalId, let hubletModel {
|
|
380
|
+
Task {
|
|
381
|
+
do {
|
|
382
|
+
try await api.deleteDeviceToken(hublet: hubletModel, token: pushToken, portalId: portalId)
|
|
383
|
+
} catch {
|
|
384
|
+
logger.error("Error deleting push token from api: \(error)")
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// either way, clear our stored token
|
|
388
|
+
self.pushToken = nil
|
|
389
|
+
self.pushTokenSyncState = .notSent
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
userIdentityToken = nil
|
|
394
|
+
userEmailAddress = nil
|
|
395
|
+
chatProperties = [:]
|
|
396
|
+
|
|
397
|
+
Task {
|
|
398
|
+
//The Hubspot chat view currently uses the default web data store
|
|
399
|
+
let cookieStore = WKWebsiteDataStore.default().httpCookieStore
|
|
400
|
+
let matchingCookies = await cookieStore.allCookies().filter { cookiesToDeleteWhenClearingData.contains($0.name) }
|
|
401
|
+
for cookie in matchingCookies {
|
|
402
|
+
await cookieStore.deleteCookie(cookie)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
objectWillChange.send()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// Computes the url for the current config for embedding chat, based on any known config for portal id, hublet, user id, etc
|
|
410
|
+
///
|
|
411
|
+
/// The chat view , ``HubspotChatView`` calls this method when setting up its embedded chat
|
|
412
|
+
/// - Parameters:
|
|
413
|
+
/// - withPushData: The struct with data from push notification
|
|
414
|
+
/// - forChatFlow: The chat flow to open.
|
|
415
|
+
/// - Returns: URL to embed to show mobile chat
|
|
416
|
+
/// - Throws: ``HubspotConfigError.missingConfiguration`` if app settings like portal id or hublet are missing, or ``HubspotConfigError.missingChatFlow`` if no chat flow is provided and no default value exists
|
|
417
|
+
func chatUrl(
|
|
418
|
+
withPushData: PushNotificationChatData?,
|
|
419
|
+
forChatFlow: String?
|
|
420
|
+
) throws -> URL {
|
|
421
|
+
guard let hublet,
|
|
422
|
+
let portalId
|
|
423
|
+
else {
|
|
424
|
+
throw HubspotConfigError.missingConfiguration
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let hubletModel = Hublet(id: hublet, environment: environment)
|
|
428
|
+
|
|
429
|
+
var components = URLComponents()
|
|
430
|
+
components.scheme = "https"
|
|
431
|
+
components.host = hubletModel.hostname
|
|
432
|
+
components.path = "/conversations-visitor-embed"
|
|
433
|
+
|
|
434
|
+
var queryItems: [String: String] = [
|
|
435
|
+
"portalId": portalId,
|
|
436
|
+
"hublet": hubletModel.id,
|
|
437
|
+
"env": environment.rawValue,
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
if let idToken = userIdentityToken {
|
|
441
|
+
queryItems["identificationToken"] = idToken
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if let email = userEmailAddress {
|
|
445
|
+
queryItems["email"] = email
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Use chat flow from push data, if exsist, otherwise use chat flow dedicated property
|
|
449
|
+
if let chatFlow = withPushData?.chatflow, !chatFlow.isEmpty {
|
|
450
|
+
queryItems["chatflow"] = chatFlow
|
|
451
|
+
} else if let chatFlow = forChatFlow, !chatFlow.isEmpty {
|
|
452
|
+
queryItems["chatflow"] = chatFlow
|
|
453
|
+
} else if let defaultChatFlow, !defaultChatFlow.isEmpty {
|
|
454
|
+
queryItems["chatflow"] = defaultChatFlow
|
|
455
|
+
} else {
|
|
456
|
+
// No chatflow, but we know we need one
|
|
457
|
+
throw HubspotConfigError.missingChatFlow
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
var urlNoPlus = CharacterSet.urlQueryAllowed
|
|
461
|
+
urlNoPlus.remove("+")
|
|
462
|
+
|
|
463
|
+
// Manually encode query string, as + is a common component in emails and we don't want + appearing un-encoded as it would be if we used the query item collection on components
|
|
464
|
+
// Note: if we ever need values with spaces, we might need to update this handling to handle values when email is/isn't the key differently
|
|
465
|
+
components.percentEncodedQuery = queryItems.compactMap {
|
|
466
|
+
guard
|
|
467
|
+
let key = $0.addingPercentEncoding(withAllowedCharacters: urlNoPlus),
|
|
468
|
+
let value = $1.addingPercentEncoding(withAllowedCharacters: urlNoPlus)
|
|
469
|
+
else {
|
|
470
|
+
return nil
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return key + "=" + value
|
|
474
|
+
}
|
|
475
|
+
.joined(separator: "&")
|
|
476
|
+
|
|
477
|
+
guard let url = components.url else {
|
|
478
|
+
throw HubspotConfigError.missingConfiguration
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return url
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/// Handle obtaining a thread id - once the thread id is known , we can post chat properties to the API. This method is used by chat views once they've extracted ID from UI / Javascript Bridge.
|
|
485
|
+
/// - Parameter threadId: the thread id retrieved from the active chat view
|
|
486
|
+
func handleThreadOpened(threadId: String) {
|
|
487
|
+
guard let portalId, let hubletModel else {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Call the api. Creating a task here instead of making this method async because right now we don't need to do anything after
|
|
492
|
+
Task {
|
|
493
|
+
// Get the properties we want to send to the api
|
|
494
|
+
let props = await finalizeChatProperties()
|
|
495
|
+
|
|
496
|
+
do {
|
|
497
|
+
try await api.sendChatProperties(
|
|
498
|
+
hublet: hubletModel,
|
|
499
|
+
properties: props,
|
|
500
|
+
visitorIdToken: self.userIdentityToken,
|
|
501
|
+
email: self.userEmailAddress,
|
|
502
|
+
threadId: threadId,
|
|
503
|
+
portalId: portalId
|
|
504
|
+
)
|
|
505
|
+
} catch {
|
|
506
|
+
logger.error("Error sending chat properties: \(error)")
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
extension Image {
|
|
513
|
+
/// Exporting chat icon - initially for demo use - but maybe sharing some resources that aren't buttons or views might be needed eventually, if so refactor this
|
|
514
|
+
public static var hubspotChat: Image {
|
|
515
|
+
Image("GenericChatIcon", bundle: .hubspotResources)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
extension Bundle {
|
|
520
|
+
static var hubspotResources: Bundle? {
|
|
521
|
+
// CocoaPods bundles resources into a separate bundle, unlike Swift Package's `Bundle.module`.
|
|
522
|
+
Bundle.main.url(forResource: "HubspotMobileSDKResources", withExtension: "bundle")
|
|
523
|
+
.flatMap { Bundle(url: $0) }
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
extension HubspotManager {
|
|
528
|
+
/// Create a visitor access token directly using app access token
|
|
529
|
+
///
|
|
530
|
+
/// Convenience for creating a visitor identity token using the given details, for situations where server infrastructure isn't available during SDK development.
|
|
531
|
+
///
|
|
532
|
+
/// > 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.
|
|
533
|
+
///
|
|
534
|
+
/// - Parameters:
|
|
535
|
+
/// - accessToken: The access token for your application, as returned by the Hubspot dashboard
|
|
536
|
+
/// - email: the email of the user
|
|
537
|
+
/// - firstName: users first name
|
|
538
|
+
/// - lastName: users last name
|
|
539
|
+
/// - Returns: The generated JWT token
|
|
540
|
+
@available(*, deprecated, message: "This is for development only and may be removed - acquiring an access token should be done as part of your products server infrastructure")
|
|
541
|
+
public func aquireUserIdentityToken(accessToken: String, email: String, firstName: String, lastName: String) async throws -> String {
|
|
542
|
+
guard let hubletModel else {
|
|
543
|
+
throw HubspotConfigError.missingConfiguration
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return try await api.createVisitorToken(hublet: hubletModel, accessToken: accessToken, email: email, firstName: firstName, lastName: lastName)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// HubspotUserProperties.swift
|
|
2
|
+
// Hubspot Mobile SDK
|
|
3
|
+
//
|
|
4
|
+
// Copyright © 2024 Hubspot, Inc.
|
|
5
|
+
|
|
6
|
+
import Foundation
|
|
7
|
+
import UIKit
|
|
8
|
+
|
|
9
|
+
/// These are the known , pre-defined key values for chat properties.
|
|
10
|
+
///
|
|
11
|
+
/// These values may be expected by Hubspot to be set in some situations, or to enable optional functionality if present.
|
|
12
|
+
///
|
|
13
|
+
/// If including chat properties in your application, if possible use these keys where appropiate rather than custom key names. Use the raw value as key values when setting chat properties on ``HubspotManager`` using ``HubspotManager/setChatProperties(data:)`` function. Use the `rawValue` property like so:
|
|
14
|
+
///
|
|
15
|
+
/// ```
|
|
16
|
+
/// manager.setChatProperties(data:[
|
|
17
|
+
/// ChatPropertyKey.location.rawValue: "....."
|
|
18
|
+
/// ])
|
|
19
|
+
/// ```
|
|
20
|
+
///
|
|
21
|
+
/// Avoid including personal, private information in property values, and only include data that you have permission from the user to use.
|
|
22
|
+
public enum ChatPropertyKey: String, Sendable {
|
|
23
|
+
/// Optional. Use this key with the value 'true' or 'false' to record if your app has camera permissions granted.
|
|
24
|
+
///
|
|
25
|
+
/// `api key: camera_permissions`
|
|
26
|
+
case cameraPermissions = "camera_permissions"
|
|
27
|
+
/// Optional. Use this key with the value 'true' or 'false' to record if your app has photo permissions granted.
|
|
28
|
+
///
|
|
29
|
+
/// `api key: photo_library_permissions`
|
|
30
|
+
case photoPermissions = "photo_library_permissions"
|
|
31
|
+
/// Automatic. This key and value is set automatically. Its set to true when there's permission granted to show notifications as alerts or in the notification centre, either as full permission or provisional permission
|
|
32
|
+
///
|
|
33
|
+
/// `api key: notification_permissions`
|
|
34
|
+
case notificationPermissions = "notification_permissions"
|
|
35
|
+
/// Optional. Use this key with the value 'true' or 'false' to record if your app has location permissions granted.
|
|
36
|
+
///
|
|
37
|
+
/// `api key: location_permissions`
|
|
38
|
+
case locationPermissions = "location_permissions"
|
|
39
|
+
/// Optional. Use this key with a location for the user, formatted as latitude,longitude , for example , "51.51148,-0.12266". This may be useful information for your support requests.
|
|
40
|
+
///
|
|
41
|
+
/// `api key: location`
|
|
42
|
+
case location
|
|
43
|
+
/// Automatic: This key value is set automatically when using push notification features of the sdk.
|
|
44
|
+
///
|
|
45
|
+
/// `api key: push_token`
|
|
46
|
+
case pushToken = "push_token"
|
|
47
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations.
|
|
48
|
+
/// Note - iOS reports model numbers differently with the device code, than how they are marketed. Instead of iPhone 15 Pro , one would get "iPhone16,1"
|
|
49
|
+
///
|
|
50
|
+
/// `api key: device_model`
|
|
51
|
+
case deviceModel = "device_model"
|
|
52
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations. The value format is the platform, for example "ios"
|
|
53
|
+
///
|
|
54
|
+
/// `api key: platform`
|
|
55
|
+
case platform
|
|
56
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations. The value format is the platform followed by version , for example "iOS 17.2"
|
|
57
|
+
///
|
|
58
|
+
/// `api key: os_version`
|
|
59
|
+
case operatingSystemVersion = "os_version"
|
|
60
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations - the value is read from the main budle version keys, formatted as (CFBundleShortVersionString).(CFBundleVersion)
|
|
61
|
+
///
|
|
62
|
+
/// `api key: app_version`
|
|
63
|
+
case appVersion = "app_version"
|
|
64
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations. Value is set to unknown, portrait, or landscape
|
|
65
|
+
///
|
|
66
|
+
/// `api key: device_orientation`
|
|
67
|
+
case deviceOrientation = "device_orientation"
|
|
68
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations. Value is formated as widthxheight
|
|
69
|
+
///
|
|
70
|
+
/// `api key: screen_size`
|
|
71
|
+
case screenSize = "screen_size"
|
|
72
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations. Value is formated as widthxheight
|
|
73
|
+
///
|
|
74
|
+
/// `api key: screen_resolution`
|
|
75
|
+
case screenResolution = "screen_resolution"
|
|
76
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations. The battery level is a percentage from 0 to 100.
|
|
77
|
+
///
|
|
78
|
+
/// `api key: battery_level`
|
|
79
|
+
case batteryLevel = "battery_level"
|
|
80
|
+
/// Automatic: This key and value is set automatically by the sdk when opening chat conversations. The value is the same as the `UIDevice.BatteryState` enum names, and can be:
|
|
81
|
+
/// unknown, full, charging, unplugged
|
|
82
|
+
///
|
|
83
|
+
/// `api key: battery_state`
|
|
84
|
+
case batteryState = "battery_state"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
extension UIDeviceOrientation {
|
|
88
|
+
var hubspotApiValue: String {
|
|
89
|
+
switch self {
|
|
90
|
+
case .portrait, .portraitUpsideDown:
|
|
91
|
+
return "portrait"
|
|
92
|
+
case .landscapeLeft, .landscapeRight:
|
|
93
|
+
return "landscape"
|
|
94
|
+
default:
|
|
95
|
+
return "unknown"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
extension UIDevice.BatteryState {
|
|
101
|
+
var hubspotApiValue: String {
|
|
102
|
+
switch self {
|
|
103
|
+
case .unknown:
|
|
104
|
+
return "unknown"
|
|
105
|
+
case .charging:
|
|
106
|
+
return "charging"
|
|
107
|
+
case .full:
|
|
108
|
+
return "full"
|
|
109
|
+
case .unplugged:
|
|
110
|
+
return "unplugged"
|
|
111
|
+
@unknown default:
|
|
112
|
+
return "unknown"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|