react-native-smartlinks 2.0.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/android/build.gradle +33 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/live/smartlinks/reactnative/SmartLinksActivityLifecycle.kt +34 -0
- package/android/src/main/java/live/smartlinks/reactnative/SmartLinksBridgeHandler.kt +172 -0
- package/android/src/main/java/live/smartlinks/reactnative/SmartLinksModule.kt +299 -0
- package/android/src/main/java/live/smartlinks/reactnative/SmartLinksPackage.kt +16 -0
- package/ios/Linklytics/DeviceInfo.swift +114 -0
- package/ios/Linklytics/Linklytics.swift +581 -0
- package/ios/Linklytics/LinklyticsInterface.swift +10 -0
- package/ios/Linklytics/Managers/BatchEventManager.swift +218 -0
- package/ios/Linklytics/Managers/OfflineCache.swift +35 -0
- package/ios/Linklytics/Managers/SQLiteStorage.swift +225 -0
- package/ios/Linklytics/Models/AppResponse.swift +16 -0
- package/ios/Linklytics/Models/DeepLinkRoute.swift +24 -0
- package/ios/Linklytics/Models/DeferredLinkResponse.swift +81 -0
- package/ios/Linklytics/Models/DynamicLinkResponse.swift +13 -0
- package/ios/Linklytics/Models/EventResponse.swift +14 -0
- package/ios/Linklytics/Models/OfflineEvent.swift +80 -0
- package/ios/Linklytics/NetworkManager.swift +360 -0
- package/ios/Linklytics/SdkConstants.swift +29 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/ios/SmartLinksModule.m +150 -0
- package/ios/SmartLinksModule.swift +281 -0
- package/package.json +41 -0
- package/react-native-smartlinks.podspec +27 -0
- package/react-native.config.js +12 -0
- package/src/index.ts +84 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class Linklytics: LinklyticsInterface {
|
|
5
|
+
|
|
6
|
+
// MARK: - Public Interface
|
|
7
|
+
public static let shared = Linklytics()
|
|
8
|
+
|
|
9
|
+
// MARK: - Constants
|
|
10
|
+
public static let sdkVersion = "1.2.5"
|
|
11
|
+
public static let apiVersion = "v2"
|
|
12
|
+
|
|
13
|
+
// MARK: - Private Properties
|
|
14
|
+
private var isInitialized = false
|
|
15
|
+
private var apiKey: String?
|
|
16
|
+
private var isDebugMode = false
|
|
17
|
+
private let userDefaults = UserDefaults.standard
|
|
18
|
+
private let prefsKey = "linklytics_prefs"
|
|
19
|
+
private var pendingUniversalLink: URL?
|
|
20
|
+
private var isAppLaunchComplete = false
|
|
21
|
+
private var networkManager: NetworkManager?
|
|
22
|
+
private var deepLinkQueue: [DeepLinkRoute] = []
|
|
23
|
+
private var linkReceivedListener: ((DeepLinkRoute) -> Void)?
|
|
24
|
+
|
|
25
|
+
private var appUserId: String? {
|
|
26
|
+
get {
|
|
27
|
+
if let saved = userDefaults.string(forKey: "\(prefsKey)_app_user_id") {
|
|
28
|
+
return saved
|
|
29
|
+
}
|
|
30
|
+
return UIDevice.current.identifierForVendor?.uuidString
|
|
31
|
+
}
|
|
32
|
+
set { userDefaults.set(newValue, forKey: "\(prefsKey)_app_user_id") }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private init() {}
|
|
36
|
+
|
|
37
|
+
// MARK: - Public Methods
|
|
38
|
+
public static var version: String {
|
|
39
|
+
return sdkVersion
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public static var apiVersionString: String {
|
|
43
|
+
return apiVersion
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public func setDebugMode(_ enabled: Bool) {
|
|
47
|
+
isDebugMode = enabled
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public func initialize(apiKey: String, debugMode: Bool = false) {
|
|
51
|
+
self.apiKey = apiKey
|
|
52
|
+
self.isDebugMode = debugMode
|
|
53
|
+
self.isInitialized = true
|
|
54
|
+
|
|
55
|
+
let networkManager = NetworkManager(apiKey: apiKey, debugMode: debugMode)
|
|
56
|
+
self.networkManager = networkManager
|
|
57
|
+
|
|
58
|
+
// Initialize BatchEventManager
|
|
59
|
+
BatchEventManager.shared.initialize(networkManager: networkManager, debugMode: debugMode)
|
|
60
|
+
|
|
61
|
+
// Check if this is first launch and handle install referrer
|
|
62
|
+
if isFirstLaunch() {
|
|
63
|
+
handleFirstLaunch()
|
|
64
|
+
} else {
|
|
65
|
+
// Mark app launch as complete for non-first launches
|
|
66
|
+
isAppLaunchComplete = true
|
|
67
|
+
|
|
68
|
+
// Handle any pending universal link that came before initialization
|
|
69
|
+
if let pendingLink = pendingUniversalLink {
|
|
70
|
+
pendingUniversalLink = nil
|
|
71
|
+
handleUniversalLink(pendingLink, isFromBackground: false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Try to send any previously cached events
|
|
76
|
+
// BatchEventManager.shared.trySendCachedEvents() // Not needed, it auto-starts monitoring
|
|
77
|
+
|
|
78
|
+
if isDebugMode {
|
|
79
|
+
print("Linklytics: SDK initialized with version \(Self.sdkVersion), API version \(Self.apiVersion)")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public func getAppUserId() -> String? {
|
|
84
|
+
return appUserId
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public func sendEvent(_ eventName: String, parameters: [String: Any?] = [:]) {
|
|
88
|
+
guard isInitialized else {
|
|
89
|
+
if isDebugMode {
|
|
90
|
+
print("Linklytics: SDK not initialized. Call initialize() first.")
|
|
91
|
+
}
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let stringParams = parameters.compactMapValues { value -> Any? in
|
|
96
|
+
// We can now support AnyCodable, so basically keep as Any but sanitize types that AnyCodable supports
|
|
97
|
+
// actually BatchEventManager takes [String: Any], which converts to OfflineEvent -> AnyCodable.
|
|
98
|
+
// So we just need to unwrap optionals.
|
|
99
|
+
return value
|
|
100
|
+
} as? [String: Any] ?? [:]
|
|
101
|
+
|
|
102
|
+
// Sanitize for AnyCodable compatibility (String, Int, Double, Bool)
|
|
103
|
+
// The previous implementation converted EVERYTHING to String.
|
|
104
|
+
// We want to PRESERVE types if possible.
|
|
105
|
+
|
|
106
|
+
var validParams: [String: Any] = [:]
|
|
107
|
+
|
|
108
|
+
for (key, value) in stringParams {
|
|
109
|
+
if let v = value as? String { validParams[key] = v }
|
|
110
|
+
else if let v = value as? Int { validParams[key] = v }
|
|
111
|
+
else if let v = value as? Double { validParams[key] = v }
|
|
112
|
+
else if let v = value as? Bool { validParams[key] = v }
|
|
113
|
+
else if let v = value as? NSNumber { validParams[key] = v } // Covers Int/Double/Bool
|
|
114
|
+
else { validParams[key] = String(describing: value) } // Fallback to string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let deviceInfo = DeviceInfo.fromCurrentDevice(
|
|
118
|
+
sdkVersion: Self.sdkVersion,
|
|
119
|
+
apiVersion: Self.apiVersion,
|
|
120
|
+
networkType: BatchEventManager.shared.getNetworkType()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Use BatchEventManager which handles Offline/Online
|
|
124
|
+
BatchEventManager.shared.sendEvent(eventName, parameters: validParams, deviceInfo: deviceInfo.toMap(), appUserId: self.appUserId)
|
|
125
|
+
|
|
126
|
+
if isDebugMode {
|
|
127
|
+
print("Linklytics: Event passed to BatchEventManager: \(eventName)")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// MARK: - Deep Link Handling
|
|
134
|
+
public func handleURL(_ url: URL) {
|
|
135
|
+
guard isInitialized else {
|
|
136
|
+
if isDebugMode {
|
|
137
|
+
print("Linklytics: SDK not initialized. Cannot handle deep link.")
|
|
138
|
+
}
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let urlString = url.absoluteString
|
|
143
|
+
var parameters: [String: String] = [:]
|
|
144
|
+
|
|
145
|
+
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
|
146
|
+
let queryItems = components.queryItems {
|
|
147
|
+
for item in queryItems {
|
|
148
|
+
parameters[item.name] = item.value ?? ""
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let deepLinkRoute = DeepLinkRoute(
|
|
153
|
+
route: urlString,
|
|
154
|
+
parameters: parameters,
|
|
155
|
+
link: urlString,
|
|
156
|
+
appSlug: getAppSlug(),
|
|
157
|
+
isFirstLaunch: false
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
addDeepLinkToQueue(deepLinkRoute)
|
|
161
|
+
|
|
162
|
+
sendEvent("deep_link_opened", parameters: [
|
|
163
|
+
"route": urlString,
|
|
164
|
+
"parameters": parameters.description
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
if isDebugMode {
|
|
168
|
+
print("Linklytics: Deep link handled: \(urlString) with parameters: \(parameters)")
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public func handleUniversalLink(_ url: URL, isFromBackground: Bool = false) {
|
|
173
|
+
// If SDK is not initialized yet, store the link for later processing
|
|
174
|
+
guard isInitialized else {
|
|
175
|
+
pendingUniversalLink = url
|
|
176
|
+
if isDebugMode {
|
|
177
|
+
print("Linklytics: Universal link received before initialization, storing for later: \(url)")
|
|
178
|
+
}
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If this is first launch and we haven't completed the launch process yet,
|
|
183
|
+
// store the link for later processing after deferred link check
|
|
184
|
+
if isFirstLaunch() && !isAppLaunchComplete {
|
|
185
|
+
pendingUniversalLink = url
|
|
186
|
+
if isDebugMode {
|
|
187
|
+
print("Linklytics: Universal link received during first launch, storing for after deferred link check: \(url)")
|
|
188
|
+
}
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let urlString = url.absoluteString
|
|
193
|
+
var parameters: [String: String] = [:]
|
|
194
|
+
|
|
195
|
+
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
|
196
|
+
let queryItems = components.queryItems {
|
|
197
|
+
for item in queryItems {
|
|
198
|
+
parameters[item.name] = item.value ?? ""
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Add context about how the link was received
|
|
203
|
+
parameters["link_source"] = isFromBackground ? "background" : "terminated"
|
|
204
|
+
|
|
205
|
+
let deepLinkRoute = DeepLinkRoute(
|
|
206
|
+
route: urlString,
|
|
207
|
+
parameters: parameters,
|
|
208
|
+
link: urlString,
|
|
209
|
+
appSlug: getAppSlug(),
|
|
210
|
+
isFirstLaunch: false
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
addDeepLinkToQueue(deepLinkRoute)
|
|
214
|
+
// Send appropriate event based on app state
|
|
215
|
+
let eventName = isFromBackground ? "universal_link_background" : "universal_link_terminated"
|
|
216
|
+
sendEvent(eventName, parameters: [
|
|
217
|
+
"url": urlString,
|
|
218
|
+
"parameters": parameters.description,
|
|
219
|
+
"app_state": isFromBackground ? "background" : "terminated"
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
if isDebugMode {
|
|
223
|
+
print("Linklytics: Universal link handled (\(isFromBackground ? "background" : "terminated")): \(urlString) with parameters: \(parameters)")
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// MARK: - Private Methods
|
|
228
|
+
private func isFirstLaunch() -> Bool {
|
|
229
|
+
let key = "\(prefsKey)_has_launched_before"
|
|
230
|
+
return !userDefaults.bool(forKey: key)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func markAsLaunched() {
|
|
234
|
+
let key = "\(prefsKey)_has_launched_before"
|
|
235
|
+
userDefaults.set(true, forKey: key)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func handleFirstLaunch() {
|
|
239
|
+
if isDebugMode {
|
|
240
|
+
print("Linklytics: Handling first launch - getting app info and checking for deferred deep links")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// First, get app info to save the app slug
|
|
244
|
+
guard let networkManager = networkManager else {
|
|
245
|
+
if isDebugMode {
|
|
246
|
+
print("Linklytics: NetworkManager not available")
|
|
247
|
+
}
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
networkManager.getApp { [weak self] result in
|
|
252
|
+
switch result {
|
|
253
|
+
case .success(let appResponse):
|
|
254
|
+
let appSlug = appResponse.appSlug
|
|
255
|
+
self?.saveAppSlug(appSlug)
|
|
256
|
+
if self?.isDebugMode == true {
|
|
257
|
+
print("Linklytics: App slug saved: \(appSlug)")
|
|
258
|
+
}
|
|
259
|
+
case .failure(let error):
|
|
260
|
+
if self?.isDebugMode == true {
|
|
261
|
+
print("Linklytics: Failed to get app info: \(error)")
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Continue with deferred link check regardless of app info result
|
|
266
|
+
self?.checkDeferredLinks()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private func checkDeferredLinks() {
|
|
271
|
+
// iOS doesn't have install referrer, so we call the deferred-links API
|
|
272
|
+
// to check if there's a pending deep link for this device
|
|
273
|
+
fetchDeferredDeepLink { [weak self] result in
|
|
274
|
+
switch result {
|
|
275
|
+
case .success(let deferredLink):
|
|
276
|
+
self?.handleDeferredDeepLink(deferredLink)
|
|
277
|
+
case .failure(let error):
|
|
278
|
+
if self?.isDebugMode == true {
|
|
279
|
+
print("Linklytics: No deferred deep link found or error: \(error)")
|
|
280
|
+
}
|
|
281
|
+
self?.handleOrganicInstall()
|
|
282
|
+
}
|
|
283
|
+
self?.markAsLaunched()
|
|
284
|
+
self?.isAppLaunchComplete = true
|
|
285
|
+
|
|
286
|
+
// Handle any pending universal link that came during first launch
|
|
287
|
+
if let pendingLink = self?.pendingUniversalLink {
|
|
288
|
+
self?.pendingUniversalLink = nil
|
|
289
|
+
self?.handleUniversalLink(pendingLink, isFromBackground: false)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private func saveAppSlug(_ appSlug: String) {
|
|
295
|
+
let key = "\(prefsKey)_app_slug"
|
|
296
|
+
userDefaults.set(appSlug, forKey: key)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private func getAppSlug() -> String? {
|
|
300
|
+
let key = "\(prefsKey)_app_slug"
|
|
301
|
+
return userDefaults.string(forKey: key)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// MARK: - Deferred Deep Links
|
|
305
|
+
|
|
306
|
+
private enum DeferredLinkError: Error {
|
|
307
|
+
case noApiKey
|
|
308
|
+
case invalidURL
|
|
309
|
+
case noData
|
|
310
|
+
case decodingError
|
|
311
|
+
case networkError(Error)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private func fetchDeferredDeepLink(completion: @escaping (Result<DeferredLinkResponse, DeferredLinkError>) -> Void) {
|
|
315
|
+
guard let networkManager = networkManager else {
|
|
316
|
+
completion(.failure(.noApiKey))
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let deviceInfo = DeviceInfo.fromCurrentDevice(
|
|
321
|
+
sdkVersion: Self.sdkVersion,
|
|
322
|
+
apiVersion: Self.apiVersion,
|
|
323
|
+
networkType: BatchEventManager.shared.getNetworkType()
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
// ✅ Generate fingerprint asynchronously
|
|
327
|
+
deviceInfo.generateFingerprint { [weak self] fingerprint in
|
|
328
|
+
guard let fingerprint = fingerprint else {
|
|
329
|
+
completion(.failure(.decodingError))
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if self?.isDebugMode == true {
|
|
334
|
+
print("Linklytics: Generated fingerprint: \(fingerprint)")
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Pass deviceInfo map to network manager
|
|
338
|
+
networkManager.fetchDeferredLink(fingerprint: fingerprint, appUserId: self?.appUserId ?? "", deviceInfo: deviceInfo.toMap()) { result in
|
|
339
|
+
switch result {
|
|
340
|
+
case .success(let data):
|
|
341
|
+
guard let data = data else {
|
|
342
|
+
completion(.failure(.noData))
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
do {
|
|
347
|
+
let deferredLink = try JSONDecoder().decode(DeferredLinkResponse.self, from: data)
|
|
348
|
+
completion(.success(deferredLink))
|
|
349
|
+
} catch {
|
|
350
|
+
if self?.isDebugMode == true {
|
|
351
|
+
print("Linklytics: Failed to decode deferred link response: \(error)")
|
|
352
|
+
}
|
|
353
|
+
completion(.failure(.decodingError))
|
|
354
|
+
}
|
|
355
|
+
case .failure(let error):
|
|
356
|
+
if self?.isDebugMode == true {
|
|
357
|
+
print("Linklytics: Failed to fetch deferred link: \(error)")
|
|
358
|
+
}
|
|
359
|
+
completion(.failure(.networkError(error)))
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
private func handleDeferredDeepLink(_ deferredLink: DeferredLinkResponse) {
|
|
367
|
+
if isDebugMode {
|
|
368
|
+
print("Linklytics: Handling deferred deep link: \(deferredLink.url ?? "no URL")")
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let route = deferredLink.url ?? "organic"
|
|
372
|
+
var parameters = deferredLink.parameters ?? [:]
|
|
373
|
+
|
|
374
|
+
// Add campaign data if available
|
|
375
|
+
if let campaign = deferredLink.campaignInfo {
|
|
376
|
+
parameters.merge(campaign) { (_, new) in new }
|
|
377
|
+
}
|
|
378
|
+
if let utmParams = deferredLink.utmParams {
|
|
379
|
+
parameters.merge(utmParams) { (_, new) in new }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let deepLinkRoute = DeepLinkRoute(
|
|
383
|
+
route: route,
|
|
384
|
+
parameters: parameters,
|
|
385
|
+
link: deferredLink.url,
|
|
386
|
+
appSlug: getAppSlug(),
|
|
387
|
+
isFirstLaunch: true
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
// Add to queue for getLink() to return
|
|
392
|
+
addDeepLinkToQueue(deepLinkRoute)
|
|
393
|
+
|
|
394
|
+
sendEvent("app_installed", parameters: [
|
|
395
|
+
"referrer": route,
|
|
396
|
+
"deferred_link": deferredLink.hasDeferredLink ? "true" : "false",
|
|
397
|
+
"parameters": parameters.description
|
|
398
|
+
])
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private func handleOrganicInstall() {
|
|
402
|
+
if isDebugMode {
|
|
403
|
+
print("Linklytics: Handling organic install")
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let deepLinkRoute = DeepLinkRoute(
|
|
407
|
+
route: "organic",
|
|
408
|
+
parameters: ["referrer": "organic"],
|
|
409
|
+
link: nil,
|
|
410
|
+
appSlug: getAppSlug(),
|
|
411
|
+
isFirstLaunch: true
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
// Add to queue for getLink() to return
|
|
415
|
+
addDeepLinkToQueue(deepLinkRoute)
|
|
416
|
+
|
|
417
|
+
sendEvent("app_installed", parameters: [
|
|
418
|
+
"referrer": "organic",
|
|
419
|
+
"deferred_link": "false"
|
|
420
|
+
])
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
private func generateLongDynamicLink(_ route: String, parameters: [String: String]? = nil) -> String{
|
|
425
|
+
guard let networkManager = networkManager else {
|
|
426
|
+
if isDebugMode {
|
|
427
|
+
print("Linklytics: NetworkManager not available")
|
|
428
|
+
}
|
|
429
|
+
return ""
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let app = getAppSlug() ?? ""
|
|
433
|
+
if(app == "") {
|
|
434
|
+
networkManager.getApp { [weak self] result in
|
|
435
|
+
switch result {
|
|
436
|
+
case .success(let appResponse):
|
|
437
|
+
let appSlug = appResponse.appSlug
|
|
438
|
+
self?.saveAppSlug(appSlug)
|
|
439
|
+
case .failure(let error):
|
|
440
|
+
if self?.isDebugMode == true {
|
|
441
|
+
print("Linklytics: Failed to get app info: \(error)")
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Return empty string for now since we can't get app slug synchronously
|
|
446
|
+
return ""
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let queryString = parameters?.map { "\($0.key)=\($0.value)" }.joined(separator: "&") ?? ""
|
|
450
|
+
let fullURL = SdkConstants.baseURL + "/" + app + "/" + route
|
|
451
|
+
return queryString.isEmpty ? fullURL : fullURL + "?" + queryString
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private func generateShortDynamicLink(_ url: String, parameters: [String: String]? = nil) -> String{
|
|
455
|
+
guard isInitialized, let networkManager = networkManager else {
|
|
456
|
+
if isDebugMode {
|
|
457
|
+
print("Linklytics: SDK not initialized. Call initialize() first.")
|
|
458
|
+
}
|
|
459
|
+
return ""
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// This is a synchronous method but we need async network call
|
|
463
|
+
// We'll use a semaphore to make it synchronous
|
|
464
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
465
|
+
var result = ""
|
|
466
|
+
|
|
467
|
+
if isDebugMode {
|
|
468
|
+
print("Linklytics: generateShortDynamicLink called with url: \(url), parameters: \(parameters ?? [:])")
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
networkManager.generateDynamicLink(url: url, parameters: parameters) { response in
|
|
472
|
+
switch response {
|
|
473
|
+
case .success(let dynamicLinkResponse):
|
|
474
|
+
result = dynamicLinkResponse.short_url ?? ""
|
|
475
|
+
if self.isDebugMode {
|
|
476
|
+
print("Linklytics: Short dynamic link generated successfully: \(result)")
|
|
477
|
+
}
|
|
478
|
+
case .failure(let error):
|
|
479
|
+
if self.isDebugMode {
|
|
480
|
+
print("Linklytics: Failed to generate short dynamic link: \(error)")
|
|
481
|
+
}
|
|
482
|
+
result = ""
|
|
483
|
+
}
|
|
484
|
+
semaphore.signal()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Wait for the async call to complete (with timeout)
|
|
488
|
+
_ = semaphore.wait(timeout: .now() + 10)
|
|
489
|
+
|
|
490
|
+
if isDebugMode {
|
|
491
|
+
print("Linklytics: generateShortDynamicLink result: \(result)")
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return result
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// MARK: - Dynamic Link Generation
|
|
498
|
+
public func generateDynamicLink(_ url: String, parameters: [String: String]? = nil, isShort: Bool = false) -> String? {
|
|
499
|
+
guard isInitialized, let networkManager = networkManager else {
|
|
500
|
+
if isDebugMode {
|
|
501
|
+
print("Linklytics: SDK not initialized. Call initialize() first.")
|
|
502
|
+
}
|
|
503
|
+
return nil
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if isDebugMode {
|
|
507
|
+
print("Linklytics: generateDynamicLink called with url: \(url), parameters: \(parameters ?? [:]), isShort: \(isShort)")
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if !isShort {
|
|
511
|
+
return generateLongDynamicLink(url, parameters: parameters)
|
|
512
|
+
} else {
|
|
513
|
+
return generateShortDynamicLink(url, parameters: parameters)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
public func getLink() -> DeepLinkRoute? {
|
|
518
|
+
guard isInitialized else {
|
|
519
|
+
if isDebugMode {
|
|
520
|
+
print("Linklytics: SDK not initialized. Call initialize() first.")
|
|
521
|
+
}
|
|
522
|
+
return nil
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Return the first deep link from the queue, if any
|
|
526
|
+
if !deepLinkQueue.isEmpty {
|
|
527
|
+
let link = deepLinkQueue.removeFirst()
|
|
528
|
+
if isDebugMode {
|
|
529
|
+
print("Linklytics: getLink returning: \(link.route)")
|
|
530
|
+
}
|
|
531
|
+
return link
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if isDebugMode {
|
|
535
|
+
print("Linklytics: getLink called but no deep links in queue")
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return nil
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
public func setOnLinkReceivedListener(_ listener: @escaping (DeepLinkRoute) -> Void) {
|
|
542
|
+
self.linkReceivedListener = listener
|
|
543
|
+
|
|
544
|
+
// Drain existing queue to the new listener
|
|
545
|
+
while !deepLinkQueue.isEmpty {
|
|
546
|
+
let link = deepLinkQueue.removeFirst()
|
|
547
|
+
if isDebugMode {
|
|
548
|
+
print("Linklytics: Draining queued link to listener: \(link.route)")
|
|
549
|
+
}
|
|
550
|
+
listener(link)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
// MARK: - Internal Deep Link Management
|
|
557
|
+
private func addDeepLinkToQueue(_ deepLinkRoute: DeepLinkRoute) {
|
|
558
|
+
if let listener = linkReceivedListener {
|
|
559
|
+
if isDebugMode {
|
|
560
|
+
print("Linklytics: Notifying listener of new deep link: \(deepLinkRoute.route)")
|
|
561
|
+
}
|
|
562
|
+
listener(deepLinkRoute)
|
|
563
|
+
} else {
|
|
564
|
+
deepLinkQueue.append(deepLinkRoute)
|
|
565
|
+
if isDebugMode {
|
|
566
|
+
print("Linklytics: Added deep link to queue: \(deepLinkRoute.route)")
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// MARK: - Testing Helper
|
|
572
|
+
public func clearUserData() {
|
|
573
|
+
let launchKey = "\(prefsKey)_has_launched_before"
|
|
574
|
+
let appSlugKey = "\(prefsKey)_app_slug"
|
|
575
|
+
userDefaults.removeObject(forKey: launchKey)
|
|
576
|
+
userDefaults.removeObject(forKey: appSlugKey)
|
|
577
|
+
if isDebugMode {
|
|
578
|
+
print("Linklytics: User data cleared for testing")
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - Public Interface
|
|
4
|
+
public protocol LinklyticsInterface {
|
|
5
|
+
func initialize(apiKey: String, debugMode: Bool)
|
|
6
|
+
func sendEvent(_ eventName: String, parameters: [String: Any?])
|
|
7
|
+
func generateDynamicLink(_ url: String, parameters: [String: String]?, isShort: Bool) -> String?
|
|
8
|
+
func getLink() -> DeepLinkRoute?
|
|
9
|
+
func setOnLinkReceivedListener(_ listener: @escaping (DeepLinkRoute) -> Void)
|
|
10
|
+
}
|