Skip to content
This repository was archived by the owner on Jun 4, 2025. It is now read-only.

Commit 3b37ab2

Browse files
committed
add notification polling
1 parent 495a1e2 commit 3b37ab2

File tree

7 files changed

+236
-85
lines changed

7 files changed

+236
-85
lines changed

Example/WeLoop/AppDelegate.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2222

2323
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
2424

25+
// Set the refresh interval to display a notification badge. Do this before initializing the SDK
26+
WeLoop.set(notificationRefreshInterval: 5.0)
27+
2528
// Set the invocation preferences. You can always change them after invoking the SDK
2629
WeLoop.set(preferredButtonPosition: .bottomRight)
2730
WeLoop.set(invocationMethod: .fab)

WeLoop/Classes/FloatingButtonController.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ class FloatingButtonController: UIViewController {
8282
view.setNeedsLayout()
8383
}
8484

85+
func setNotificationBadge(hidden: Bool) {
86+
self.button.setBadge(hidden: hidden)
87+
}
88+
8589
@objc func keyboardDidShow(notification: NSNotification) {
8690
window.windowLevel = UIWindow.Level(rawValue: 0)
8791
}

WeLoop/Classes/WeLoop+Authentication.swift

Lines changed: 0 additions & 77 deletions
This file was deleted.

WeLoop/Classes/WeLoop.swift

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class WeLoop: NSObject {
3838
private var project: Project?
3939

4040
/// The current app user. Must be set before the widget can be loaded
41-
private var user: User?
41+
internal var user: User?
4242

4343
/// A ref to an error that occurred during the authentication. Will be passed down to the delegate the next time `invoke` is called
4444
private var authenticationError: Error?
@@ -53,6 +53,9 @@ public class WeLoop: NSObject {
5353

5454
/// The preferred invocation method for the SDK. Must be set using `setInvocationMethod`
5555
var invocationMethod: WeLoopInvocation = .manual
56+
57+
/// The time interval between each notification refresh call. Must be set using `setNotificationRefreshInterval`
58+
var refreshInterval: TimeInterval = 30.0
5659

5760
// MARK: Object references
5861

@@ -66,6 +69,9 @@ public class WeLoop: NSObject {
6669
/// A reference to the controller containing the floating action button
6770
private var fabController: FloatingButtonController?
6871

72+
/// A reference to the polling timer to refresh notifications
73+
private var notificationRefreshTimer: Timer?
74+
6975
/// A screenshot of the window is taken right before invoking the SDK. This is a ref to this screenshot
7076
var screenshot: UIImage?
7177

@@ -125,18 +131,26 @@ public class WeLoop: NSObject {
125131
shared.fabController?.updatePosition(position)
126132
}
127133

134+
/// Set the preferred time interval between two calls to refresh the notifications on the weloop project.
135+
/// You **must** call this before calling `initialize` if you wish to customize this parameter.
136+
///
137+
/// - Parameter position: the desired time elapsed between each notification refresh
138+
@objc public static func set(notificationRefreshInterval interval: TimeInterval) {
139+
shared.refreshInterval = interval
140+
}
128141

129142
// MARK: - Internal API
130143

131-
132144
/// Initializer is made private to prevent clients from creating any other instances
133145
private override init() {
134146
super.init()
147+
148+
NotificationCenter.default.addObserver(self, selector: #selector(startNotificationPolling), name: UIApplication.willEnterForegroundNotification, object: nil)
149+
NotificationCenter.default.addObserver(self, selector: #selector(stopNotificationPolling), name: UIApplication.willResignActiveNotification, object: nil)
135150
}
136151

137-
var isShowingWidget: Bool {
138-
guard let widgetVC = weLoopViewController else { return false }
139-
return widgetVC.window.isKeyWindow && previousWindow == nil
152+
deinit {
153+
NotificationCenter.default.removeObserver(self)
140154
}
141155

142156
func initialize(apiKey: String, autoAuthentication: Bool = true, subdomain: String? = nil) {
@@ -152,6 +166,7 @@ public class WeLoop: NSObject {
152166
self.project = project
153167
self.setupInvocation(settings: project.settings)
154168
try self.initializeWidget()
169+
self.startNotificationPolling()
155170
self.delegate?.initializationSuccessful?()
156171
} catch (let error) {
157172
self.authenticationError = error
@@ -162,6 +177,8 @@ public class WeLoop: NSObject {
162177
dataTask?.resume()
163178
}
164179

180+
// MARK: Invocation
181+
165182
func set(invocationMethod method: WeLoopInvocation) {
166183
let oldInvocation = invocationMethod
167184
invocationMethod = method
@@ -178,7 +195,6 @@ public class WeLoop: NSObject {
178195
guard !isShowingWidget else { return }
179196
try showWidget()
180197
} catch (let error) {
181-
print(error)
182198
delegate?.failedToLaunch?(with: error)
183199
}
184200
}
@@ -207,6 +223,13 @@ public class WeLoop: NSObject {
207223
}
208224
}
209225

226+
// MARK: Widget
227+
228+
var isShowingWidget: Bool {
229+
guard let widgetVC = weLoopViewController else { return false }
230+
return widgetVC.window.isKeyWindow && previousWindow == nil
231+
}
232+
210233
private func initializeWidget() throws {
211234
let url = try widgetURL()
212235
let widgetVC = WeLoopViewController()
@@ -241,8 +264,7 @@ public class WeLoop: NSObject {
241264

242265
let settingsParams = try project.settings.queryParams()
243266

244-
var urlString = "\(appURL())/\(apiKey)/project/conversations?params=\(settingsParams)"
245-
267+
var urlString = "\(appURL)/\(apiKey)/project/conversations?params=\(settingsParams)"
246268
if autoAuthentication, let user = user {
247269
let userParams = try user.queryParams()
248270
urlString.append("&auto=\(userParams)")
@@ -251,6 +273,30 @@ public class WeLoop: NSObject {
251273
}
252274
return URL(string: urlString)!
253275
}
276+
277+
// MARK: Notification Badge
278+
279+
@objc private func startNotificationPolling() {
280+
notificationRefreshTimer?.invalidate()
281+
notificationRefreshTimer = Timer(timeInterval: refreshInterval, target: self, selector: #selector(refreshNotificationBadge), userInfo: nil, repeats: true)
282+
RunLoop.main.add(notificationRefreshTimer!, forMode: .default)
283+
}
284+
285+
@objc private func stopNotificationPolling() {
286+
notificationRefreshTimer?.invalidate()
287+
notificationRefreshTimer = nil
288+
}
289+
290+
@objc func refreshNotificationBadge() {
291+
refreshNotificationCount { [weak self] (response) in
292+
do {
293+
let notification = try response()
294+
self?.fabController?.setNotificationBadge(hidden: !notification.isNotif)
295+
} catch let (error) {
296+
print(error)
297+
}
298+
}
299+
}
254300
}
255301

256302
extension WeLoop: ShakeGestureDelegate {

WeLoop/Classes/WeLoopAPI.swift

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//
2+
// Authentication.swift
3+
// WeLoop
4+
//
5+
// Created by Henry Huck on 04/04/2019.
6+
//
7+
8+
import Foundation
9+
10+
struct AuthenticationResponse: Decodable {
11+
let access: Bool
12+
let project: Project
13+
}
14+
15+
struct NotificationResponse: Decodable {
16+
let success: Bool
17+
let isNotif: Bool
18+
19+
private enum CodingKeys: String, CodingKey {
20+
case isNotif = "IsNotif"
21+
case success = "success"
22+
}
23+
}
24+
25+
typealias AuthenticationCallback = (_ project:() throws -> Project) -> Void
26+
typealias NotificationCallback = (_ notification:() throws -> NotificationResponse) -> Void
27+
28+
// API Methods to communicate with the weLoop API. This is part of the weloop object since their number are limited,
29+
// but should be splitted into a dedicated object if it were to become bigger
30+
extension WeLoop {
31+
32+
// Authentication
33+
34+
func authenticate(completionHandler: @escaping AuthenticationCallback) -> URLSessionDataTask? {
35+
36+
guard let request = authenticationRequest() else {
37+
return nil
38+
}
39+
let autoAuthentication = self.autoAuthentication
40+
41+
return URLSession(configuration: .default).dataTask(with: request, completionHandler: { (data, response, error) in
42+
do {
43+
if let error = error { throw error }
44+
guard let data = data else { throw WeLoopError.authenticationDataMissing }
45+
46+
let response = try JSONDecoder().decode(AuthenticationResponse.self, from: data)
47+
if response.access || !autoAuthentication {
48+
DispatchQueue.main.async(execute: { () -> Void in
49+
completionHandler({response.project})
50+
})
51+
} else {
52+
throw WeLoopError.accessDenied
53+
}
54+
} catch (let error) {
55+
DispatchQueue.main.async(execute: { () -> Void in
56+
completionHandler({ throw error})
57+
})
58+
}
59+
})
60+
}
61+
62+
private func authenticationRequest() -> URLRequest? {
63+
64+
guard let apiKey = apiKey else { return nil }
65+
66+
var urlRequest = URLRequest(url: authenticationURL)
67+
urlRequest.httpMethod = "POST";
68+
urlRequest.setValue("Application/json", forHTTPHeaderField: "Content-Type")
69+
70+
guard let httpBody = try? JSONSerialization.data(withJSONObject: ["AutoAuthentication": autoAuthentication, "ProjectGuid": apiKey]) else {
71+
return nil
72+
}
73+
urlRequest.httpBody = httpBody
74+
return urlRequest
75+
}
76+
77+
// Notification Refresh
78+
79+
func refreshNotificationCount(completionHandler: @escaping NotificationCallback) {
80+
guard let request = refreshNotificationCountRequest() else {
81+
return
82+
}
83+
84+
let task = URLSession(configuration: .default).dataTask(with: request, completionHandler: { (data, response, error) in
85+
do {
86+
if let error = error { throw error }
87+
guard let data = data else { throw WeLoopError.notificationDataMissing }
88+
let response = try JSONDecoder().decode(NotificationResponse.self, from: data)
89+
DispatchQueue.main.async(execute: { () -> Void in
90+
completionHandler({ response })
91+
})
92+
} catch (let error) {
93+
DispatchQueue.main.async(execute: { () -> Void in
94+
completionHandler({ throw error})
95+
})
96+
}
97+
})
98+
task.resume()
99+
}
100+
101+
private func refreshNotificationCountRequest() -> URLRequest? {
102+
guard let notificationURL = notificationURL else { return nil }
103+
var urlRequest = URLRequest(url: notificationURL)
104+
urlRequest.httpMethod = "GET";
105+
urlRequest.setValue("Application/json", forHTTPHeaderField: "Content-Type")
106+
return urlRequest
107+
}
108+
109+
// Router
110+
111+
private var authenticationURL: URL {
112+
return URL(string: "\(apiURL)/UnauthorizedData/checkProjectIsVisible")!
113+
}
114+
115+
private var notificationURL: URL? {
116+
get {
117+
guard let email = user?.email, let apiKey = apiKey else { return nil }
118+
return URL(string: "\(apiURL)/UnauthorizedData/Notif?email=\(email)&projectGuid=\(apiKey)")!
119+
}
120+
}
121+
122+
private var apiURL: String {
123+
get {
124+
if let subdomain = subdomain {
125+
return "https://\(subdomain)-api.getweloop.io/api"
126+
} else {
127+
return "https://api.getweloop.io/api"
128+
}
129+
}
130+
}
131+
132+
var appURL: String {
133+
get {
134+
if let subdomain = subdomain {
135+
return "https://\(subdomain).getweloop.io/app/plugin/index/#"
136+
} else {
137+
return "https://app.getweloop.io/app/plugin/index/#"
138+
}
139+
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)