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,145 @@
1
+ // HubspotConfig.swift
2
+ // Hubspot Mobile SDK
3
+ //
4
+ // Copyright © 2024 Hubspot, Inc.
5
+
6
+ import Foundation
7
+
8
+ /// Enum used during configuration. The default is production - if in doubt choose production
9
+ public enum HubspotEnvironment: String, Codable, CustomStringConvertible, Sendable {
10
+ /// QA environment , mostly for internal use
11
+ case qa
12
+ /// Production environment, the most commonly used environment
13
+ case production = "prod"
14
+
15
+ /// Display friendly name for the environment
16
+ public var description: String {
17
+ switch self {
18
+ case .production:
19
+ return "Production"
20
+ case .qa:
21
+ return "QA"
22
+ }
23
+ }
24
+ }
25
+
26
+ /// Encapsulates some of the logic around hublets, as some are treated differently than others. Right now we know of only 2, but this might expand in the future?
27
+ struct Hublet {
28
+ /// This is the default(?) hublet, it uses just plain sub domain
29
+ let defaultUS = "na1"
30
+
31
+ let id: String
32
+ let environment: HubspotEnvironment
33
+
34
+ /// The format of subdomain varies between hublets
35
+ private var appsSubDomain: String {
36
+ let id = id.lowercased()
37
+ if id == defaultUS {
38
+ return "app"
39
+ } else {
40
+ // other hublets like eu1 have hublet in the subdomain
41
+ return "app-\(id)"
42
+ }
43
+ }
44
+
45
+ private var appsDomain: String {
46
+ // Right now, qa env has its own domain
47
+ switch environment {
48
+ case .production:
49
+ return "hubspot.com"
50
+ case .qa:
51
+ return "hubspotqa.com"
52
+ }
53
+ }
54
+
55
+ /// The format of subdomain varies between hublets
56
+ private var apiSubDomain: String {
57
+ let id = id.lowercased()
58
+ if id == defaultUS {
59
+ return "api"
60
+ } else {
61
+ // other hublets like eu1 have hublet in the subdomain
62
+ return "api-\(id)"
63
+ }
64
+ }
65
+
66
+ private var apiDomain: String {
67
+ // Right now, qa env has its own domain
68
+ switch environment {
69
+ case .production:
70
+ return "hubapi.com"
71
+ case .qa:
72
+ return "hubapiqa.com"
73
+ }
74
+ }
75
+
76
+ /// hostname used for the embedded chat page
77
+ var hostname: String {
78
+ return appsSubDomain + "." + appsDomain
79
+ }
80
+
81
+ /// hostname used for api calls
82
+ var apiHostname: String {
83
+ return apiSubDomain + "." + apiDomain
84
+ }
85
+
86
+ /// base url for api calls - append path before using
87
+ var apiURL: URL {
88
+ var components = URLComponents()
89
+ components.scheme = "https"
90
+ components.host = apiHostname
91
+ guard let url = components.url else {
92
+ fatalError("Unable to build URL from configuration")
93
+ }
94
+ return url
95
+ }
96
+ }
97
+
98
+ /// Errors relating to setting up SDK
99
+ public enum HubspotConfigError: LocalizedError, Sendable {
100
+ /// Missing config file, or missing value within - if this error occurs, make sure hubspot info file is bundled in app, and that the manager configure method ``HubspotManager/configure()-swift.type.method`` has been called .
101
+ case missingConfiguration
102
+
103
+ /// Chat flow is needed to correctly show a chat
104
+ case missingChatFlow
105
+
106
+ /// Description of the error and reason for failure, same as ``failureReason`` currently.
107
+ public var errorDescription: String? {
108
+ switch self {
109
+ case .missingConfiguration:
110
+ return "Couldn't find a configuration at the expected path \(HubspotConfig.defaultConfigFileName)"
111
+ case .missingChatFlow:
112
+ return "No chat flow provided, and no default found"
113
+ }
114
+ }
115
+ /// Description of the error and reason for failure, same as ``errorDescription`` currently.
116
+ public var failureReason: String? {
117
+ switch self {
118
+ case .missingConfiguration:
119
+ return "Couldn't find a configuration at the expected path \(HubspotConfig.defaultConfigFileName)"
120
+ case .missingChatFlow:
121
+ return "No chat flow provided, and no default found"
122
+ }
123
+ }
124
+ }
125
+
126
+ /// This struct for decoding the config file bundled in app - the config file contains the required pieces of info needed to connect to the correct hubspot endponts like that , account specific info like portal id and hublet
127
+ ///
128
+ /// By default, the SDK will initialise using a known file path, Hubspot-Info.plist
129
+ ///
130
+ public struct HubspotConfig: Codable, Sendable {
131
+ /// This is the default, assumed filename for the plist bunded in app containing the config values
132
+ public static let defaultConfigFileName: String = "Hubspot-Info.plist"
133
+
134
+ /// The hubspot environment to use
135
+ public let environment: HubspotEnvironment
136
+
137
+ /// The hublet the portal is in, for example "na1" or "eu1"
138
+ public let hublet: String
139
+
140
+ /// The unique id for the customers hubspot portal
141
+ public let portalId: String
142
+
143
+ /// The default chat flow value to use if not specified when creating a chat view
144
+ public let defaultChatFlow: String?
145
+ }
@@ -0,0 +1,178 @@
1
+ // HubspotManager+Notifications.swift
2
+ // Hubspot Mobile SDK
3
+ //
4
+ // Copyright © 2024 Hubspot, Inc.
5
+
6
+ import Foundation
7
+ import NotificationCenter
8
+
9
+ extension HubspotManager {
10
+ /// Alternative to ``newMessage`` publisher or ``newMessageCallback`` property , potentially useful in a SwiftUI view
11
+ ///
12
+ /// This could be used in a stand alone task like so:
13
+ ///
14
+ /// ```swift
15
+ /// Task {
16
+ /// for await notification in HubspotManager.shared.newMessages() {
17
+ /// self.triggerChatFlow()
18
+ /// }
19
+ ///
20
+ /// }
21
+ /// ```
22
+ /// or attached to a view:
23
+ /// ```swift
24
+ /// myView
25
+ /// .frame(...)
26
+ /// .background(...)
27
+ /// .task {
28
+ /// for await _ in HubspotManager.shared.newMessages() {
29
+ /// presentChatView = true
30
+ /// }
31
+ /// }
32
+ /// ```
33
+ /// > Warning: This async stream normally will never terminate.
34
+ ///
35
+ public func newMessages() -> AsyncStream<PushNotificationChatData> {
36
+ return AsyncStream { cont in
37
+
38
+ // Supressing sendability issue with cancellable - should be ok to send to the onTermination, as nothing else will have a reference to it
39
+ nonisolated(unsafe) let pubCancellable = self.newMessage.sink(
40
+ receiveCompletion: { _ in
41
+ // doesn't matter if the subscription fails or finishes, our sequence is done - in this specific case we expect it never to do either , but just incase, end the stream.
42
+ cont.finish()
43
+ },
44
+ receiveValue: { data in
45
+ cont.yield(data)
46
+ })
47
+
48
+ cont.onTermination = { termination in
49
+ switch termination {
50
+ case .cancelled:
51
+ // If the termination was from the task / stream side , the subscription itself is still active, so attempt to cancel it
52
+ pubCancellable.cancel()
53
+ case .finished:
54
+ // Do nothing if we finished the stream - that only happens when the subscription itself finished
55
+ break
56
+ @unknown default:
57
+ pubCancellable.cancel()
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ /// This is a convenience method for allowing this HubspotManager instance to act as your UNUserNotificationCenterDelegate. This will check any notifications received for hubspot messages.
64
+ ///
65
+ /// If using the HubspotManager to handle notifications, call this before the end of app launch to ensure all notifications are handled.
66
+ ///
67
+ /// If you would prefer to present notification permission dialog at a specific time, or as part of a primer screen, set promptForNotificationPermissions to false. If you have no preference for when to prompt the user , set promptForNotificationPermissions to true, and the user will be prompted if permissions are not yet granted.
68
+ ///
69
+ /// It's also ok to call this multiple times - you might call `configurePushMessaging(promptForNotificationPermissions: false, allowProvisionalNotifications: true, newMessageCallback: myHandler)` during your app start up or initial setup, and later after user enables a setting or accesses a particular feature, call `configurePushMessaging(promptForNotificationPermissions: true, allowProvisionalNotifications: true, newMessageCallback: myHandler)` to trigger the prompt for permissions.
70
+ ///
71
+ /// **Warning** This will set the delegate for the current UNUserNotificationCenter - if you need to handle UNUserNotificationCenterDelegate callbacks in your app, do not use this method.
72
+ ///
73
+ /// If the app should show chat UI in response to the user tapping a notification, newMessageCallback is triggered. Use this closure to configure your UI.
74
+ /// - Parameters:
75
+ /// - promptForNotificationPermissions: If true, and notification permissions are not yet granted, the user is prompted to allow notifications
76
+ /// - allowProvisionalNotifications: If `promptForNotificationPermissions` is false, set this to true to enable provisional notifications if not already granted. Has no effect if `promptForNotificationPermissions` is true.
77
+ /// - newMessageCallback: Use this closure to configure your UI to show chat view. Called on the main thread. If nil, the call back isn't changed from any previous configuration. Alternatively, leave as nil , and set ``HubspotManager/newMessageCallback`` property directly. or use the ``HubspotManager/newMessage`` publisher property
78
+ public func configurePushMessaging(
79
+ promptForNotificationPermissions: Bool,
80
+ allowProvisionalNotifications: Bool,
81
+ newMessageCallback: ((PushNotificationChatData) -> Void)? = nil
82
+ ) {
83
+ UIApplication.shared.registerForRemoteNotifications()
84
+
85
+ // Act as the delegate for opening notifications so we can tell when someone opens one.
86
+ UNUserNotificationCenter.current().delegate = self
87
+
88
+ if promptForNotificationPermissions {
89
+ Task.detached {
90
+ let currentSettings = await UNUserNotificationCenter.current().notificationSettings()
91
+ // We only want to request auth if not yet asked or just provisional
92
+ if currentSettings.authorizationStatus == .notDetermined || currentSettings.authorizationStatus == .provisional {
93
+ do {
94
+ let authorized = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
95
+ if authorized {
96
+ await self.logger.trace("Notification permission granted")
97
+ }
98
+ } catch {
99
+ await self.logger.error("Unable to request notification permissions: \(error)")
100
+ }
101
+ }
102
+ }
103
+ } else if allowProvisionalNotifications {
104
+ Task.detached {
105
+ let currentSettings = await UNUserNotificationCenter.current().notificationSettings()
106
+ // We only want to request auth if not yet asked
107
+ if currentSettings.authorizationStatus == .notDetermined {
108
+ do {
109
+ let authorized = try await UNUserNotificationCenter.current().requestAuthorization(options: [.provisional])
110
+ if authorized {
111
+ await self.logger.trace("Provisional notification settings enabled")
112
+ }
113
+ } catch {
114
+ await self.logger.error("Unable to request notification permissions: \(error)")
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ if let newMessageCallback {
121
+ self.newMessageCallback = newMessageCallback
122
+ }
123
+ }
124
+ }
125
+
126
+ /// Making the `HubspotManager` your user notification delete is not required, but its an option for convenience in situations where another delegate doesn't already exist.
127
+ extension HubspotManager: UNUserNotificationCenterDelegate {
128
+ /// Use this method to help identify incoming notifications that are hubspot related , incase you wish to handle them differently
129
+ public nonisolated func isHubspotNotification(notification: UNNotification) -> Bool {
130
+ let notificationData = notification.request.content.userInfo
131
+ return isHubspotNotification(notificationData: notificationData)
132
+ }
133
+
134
+ /// Use this method to help identify incoming notifications that are hubspot related , incase you wish to handle them differently
135
+ public nonisolated func isHubspotNotification(notificationData: [AnyHashable: Any]) -> Bool {
136
+ let hasAHubspotKey = notificationData.contains(where: { key, _ in
137
+ guard let key = key as? String else {
138
+ return false
139
+ }
140
+
141
+ // There's a few of keys we can potentially have here
142
+ return
143
+ key.hasPrefix(PushNotificationChatData.chatflowIdKey) || key.hasPrefix(PushNotificationChatData.chatflowKey) || key.hasPrefix(PushNotificationChatData.portalIdKey) || key.hasPrefix(PushNotificationChatData.threadIdKey)
144
+ })
145
+
146
+ return hasAHubspotKey
147
+ }
148
+
149
+ /// A ``HubspotManager`` instanance, like ``HubspotManager/shared`` can be used as a notification centre delegate, in situations where all notifications are from hubspot. If you have your own notification delegate, instead call this method from within your own delegate for notifications that are hubspot related.
150
+ ///
151
+ ///
152
+ public nonisolated func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
153
+ if isHubspotNotification(notification: response.notification) {
154
+ guard let chatData = PushNotificationChatData(notification: response.notification) else {
155
+ // none of the expected data in the message
156
+ return
157
+ }
158
+
159
+ // dispatching on main as most likely will result in ui changes - incase not everyone remembers to enforce processing on main thread for presentations
160
+ DispatchQueue.main.async {
161
+ self.newMessageCallback(chatData)
162
+ self.newMessage.send(chatData)
163
+ }
164
+ } else {
165
+ let requestId = response.notification.request.identifier
166
+ Task {
167
+ await self.logger.info("Push message handled by HubspotManager that isn't detected as as Hubspot notifiation. This may be a misconfiguration. Response id: \(requestId)")
168
+ }
169
+ }
170
+ completionHandler()
171
+ }
172
+
173
+ /// A ``HubspotManager`` instanance, like ``HubspotManager/shared`` can be used as a notification centre delegate, in situations where all notifications are from hubspot. If you have your own notification delegate, instead call this method from within your own delegate for notifications that are hubspot related.
174
+ ///
175
+ public nonisolated func userNotificationCenter(_: UNUserNotificationCenter, willPresent _: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
176
+ completionHandler([.banner, .sound])
177
+ }
178
+ }
@@ -0,0 +1,53 @@
1
+ // HubspotManager+Properties.swift
2
+ // Hubspot Mobile SDK
3
+ //
4
+ // Copyright © 2024 Hubspot, Inc.
5
+
6
+ import Foundation
7
+ import UIKit // Needed for UIDevice
8
+
9
+ extension HubspotManager {
10
+ /// Called by ui components if a chat might potentially happen - allows manager class to enable anything that needs prep time - like battery monitoring ahead of gathering it.
11
+ /// Internal for now - if its not enough to be called in the chat view, it can be made public and a requirement to be called from custom components.
12
+ func prepareForPotentialChat() {
13
+ enableBatteryMonitoring()
14
+ enableOrientationMonitoring()
15
+ }
16
+
17
+ func enableOrientationMonitoring() {
18
+ if !UIDevice.current.isGeneratingDeviceOrientationNotifications {
19
+ didWeEnableOrientationMonitoring = true
20
+ UIDevice.current.beginGeneratingDeviceOrientationNotifications()
21
+ }
22
+ }
23
+
24
+ func enableBatteryMonitoring() {
25
+ if !UIDevice.current.isBatteryMonitoringEnabled {
26
+ didWeEnableBatterMonitoring = true
27
+ UIDevice.current.isBatteryMonitoringEnabled = true
28
+ }
29
+ }
30
+
31
+ /// Fetch the system model , converting c struct into normal string. Uses utsname function and reflection.
32
+ /// The result is the apple model number, like iPhone15,4 , iPhone16,1 , etc rather than marketing name like "Pro Max"
33
+ func deviceModel() -> String {
34
+ var info = utsname() // create empty struct
35
+ uname(&info) // populate it
36
+
37
+ /// We can't iterate over a tuple of characters, nor can we use the init methods that take an array of cchars, so reflection to loop over them instead
38
+ let mirror = Mirror(reflecting: info.machine)
39
+ let parts: [String] = mirror.children.compactMap { charProperty -> String? in
40
+ // 0 would be end of string
41
+ guard let intChar = charProperty.value as? Int8, intChar > 0 else {
42
+ return nil
43
+ }
44
+
45
+ guard let scalar = UnicodeScalar(UInt32(intChar)) else {
46
+ return nil
47
+ }
48
+ return String(scalar)
49
+ }
50
+
51
+ return parts.joined()
52
+ }
53
+ }