react-native-cliqit 0.0.1
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/README.md +209 -0
- package/android/build.gradle +27 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/cliqit/CliqItModule.kt +242 -0
- package/android/src/main/java/com/cliqit/CliqItPackage.kt +14 -0
- package/android/src/main/java/com/cliqit/sdk/CliqItDefaults.kt +17 -0
- package/android/src/main/java/com/cliqit/sdk/CliqItDeviceInfo.kt +196 -0
- package/android/src/main/java/com/cliqit/sdk/CliqItHttp.kt +98 -0
- package/android/src/main/java/com/cliqit/sdk/CliqItModels.kt +130 -0
- package/android/src/main/java/com/cliqit/sdk/CliqItSDK.kt +514 -0
- package/android/src/main/java/com/cliqit/sdk/CliqItWebFingerprint.kt +230 -0
- package/ios/CliqItModule.m +14 -0
- package/ios/CliqItModule.swift +238 -0
- package/ios/Frameworks/CliqIt.xcframework/Info.plist +44 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/CliqIt +0 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/Headers/CliqIt-Swift.h +382 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/Info.plist +0 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios.abi.json +12111 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios.private.swiftinterface +419 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios.swiftinterface +419 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64/CliqIt.framework/Modules/module.modulemap +3 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/CliqIt +0 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Headers/CliqIt-Swift.h +760 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Info.plist +0 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios-simulator.abi.json +12111 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +419 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/arm64-apple-ios-simulator.swiftinterface +419 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/x86_64-apple-ios-simulator.abi.json +12111 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +419 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/CliqIt.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +419 -0
- package/ios/Frameworks/CliqIt.xcframework/ios-arm64_x86_64-simulator/CliqIt.framework/Modules/module.modulemap +3 -0
- package/package.json +48 -0
- package/react-native-cliqit.podspec +27 -0
- package/react-native.config.js +11 -0
- package/src/index.d.ts +92 -0
- package/src/index.js +152 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
package com.cliqit.sdk
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
import java.util.concurrent.Executors
|
|
10
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Android port of iOS CliqItSDK — configure, deferred match, direct link lookup, handle(url).
|
|
14
|
+
*/
|
|
15
|
+
object CliqItSDK {
|
|
16
|
+
private const val TAG = "CliqIt"
|
|
17
|
+
|
|
18
|
+
@Volatile private var appContext: Context? = null
|
|
19
|
+
@Volatile private var apiKey: String? = null
|
|
20
|
+
|
|
21
|
+
private var deepLinkHandler: ((CliqItPayload) -> Unit)? = null
|
|
22
|
+
private var deferredHandler: ((DeferredMatchOutcome) -> Unit)? = null
|
|
23
|
+
private var linkLookupHandler: ((LinkLookupResult) -> Unit)? = null
|
|
24
|
+
private var verifyHandler: ((VerifyOutcome) -> Unit)? = null
|
|
25
|
+
|
|
26
|
+
private var pendingPayload: CliqItPayload? = null
|
|
27
|
+
private var lastLinkDetails: LinkDetails? = null
|
|
28
|
+
private var lastDeferredOutcome: DeferredMatchOutcome? = null
|
|
29
|
+
private var lastVerifyJSON: String? = null
|
|
30
|
+
private var lastVerifyOutcome: VerifyOutcome? = null
|
|
31
|
+
private var launchClickSessionId: String? = null
|
|
32
|
+
private var receivedDirectThisSession = false
|
|
33
|
+
private val matchInFlight = AtomicBoolean(false)
|
|
34
|
+
|
|
35
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
36
|
+
private val io = Executors.newSingleThreadExecutor()
|
|
37
|
+
|
|
38
|
+
val isConfigured: Boolean get() = apiKey != null && appContext != null
|
|
39
|
+
|
|
40
|
+
val hasDeferredMatchBeenReported: Boolean
|
|
41
|
+
get() = prefs()?.getBoolean(CliqItDefaults.KEY_MATCH_REPORTED, false) == true
|
|
42
|
+
|
|
43
|
+
val isDeferredMatchInFlight: Boolean get() = matchInFlight.get()
|
|
44
|
+
|
|
45
|
+
fun init(context: Context) {
|
|
46
|
+
appContext = context.applicationContext
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun configure(apiKey: String) {
|
|
50
|
+
if (appContext == null) {
|
|
51
|
+
throw IllegalStateException("Call CliqItSDK.init(context) before configure")
|
|
52
|
+
}
|
|
53
|
+
this.apiKey = apiKey
|
|
54
|
+
Log.i(TAG, "✅ configured")
|
|
55
|
+
verifyInBackground()
|
|
56
|
+
beginDeferredMatchIfNeeded()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun onDeepLink(handler: (CliqItPayload) -> Unit) {
|
|
60
|
+
deepLinkHandler = handler
|
|
61
|
+
pendingPayload?.let {
|
|
62
|
+
pendingPayload = null
|
|
63
|
+
mainHandler.post { handler(it) }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fun onDeferredMatch(handler: (DeferredMatchOutcome) -> Unit) {
|
|
68
|
+
deferredHandler = handler
|
|
69
|
+
lastDeferredOutcome?.let { outcome ->
|
|
70
|
+
mainHandler.post { handler(outcome) }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fun onDirectLinkLookup(handler: (LinkLookupResult) -> Unit) {
|
|
75
|
+
linkLookupHandler = handler
|
|
76
|
+
lastLinkDetails?.let { details ->
|
|
77
|
+
mainHandler.post { handler(LinkLookupResult.Resolved(details)) }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun onVerify(handler: (VerifyOutcome) -> Unit) {
|
|
82
|
+
verifyHandler = handler
|
|
83
|
+
lastVerifyOutcome?.let { outcome ->
|
|
84
|
+
mainHandler.post { handler(outcome) }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun handle(urlString: String): Boolean {
|
|
89
|
+
val uri = Uri.parse(urlString) ?: return false
|
|
90
|
+
return handle(uri)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun handle(uri: Uri): Boolean {
|
|
94
|
+
captureClickSessionId(uri)
|
|
95
|
+
val key = apiKey
|
|
96
|
+
val ctx = appContext
|
|
97
|
+
if (key == null || ctx == null) {
|
|
98
|
+
pendingPayload = parsePayload(uri, source = "unknown", isDeferred = false)
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!isAllowedUrl(uri)) {
|
|
103
|
+
Log.i(TAG, "Ignored unsupported URL: $uri")
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
val payload = parsePayload(uri, source = detectSource(uri), isDeferred = false)
|
|
108
|
+
receivedDirectThisSession = true
|
|
109
|
+
|
|
110
|
+
val slug = slugFrom(uri)
|
|
111
|
+
if (slug != null) {
|
|
112
|
+
fetchLinkDetails(key, uri, slug, payload)
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
deliver(payload)
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun parseVerifyOutcome(body: String): VerifyOutcome {
|
|
121
|
+
return try {
|
|
122
|
+
val root = JSONObject(body)
|
|
123
|
+
val checksObj = root.optJSONObject("checks")
|
|
124
|
+
val checks = mutableMapOf<String, VerifyCheck>()
|
|
125
|
+
if (checksObj != null) {
|
|
126
|
+
val keys = checksObj.keys()
|
|
127
|
+
while (keys.hasNext()) {
|
|
128
|
+
val key = keys.next()
|
|
129
|
+
val item = checksObj.optJSONObject(key) ?: continue
|
|
130
|
+
checks[key] = VerifyCheck(
|
|
131
|
+
actual = CliqItHttp.optString(item, "actual"),
|
|
132
|
+
expected = CliqItHttp.optString(item, "expected"),
|
|
133
|
+
match = item.optBoolean("match", false),
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
val result = VerifyResult(
|
|
138
|
+
ok = root.optBoolean("ok", false),
|
|
139
|
+
appId = CliqItHttp.optString(root, "appId"),
|
|
140
|
+
appName = CliqItHttp.optString(root, "appName"),
|
|
141
|
+
checks = checks,
|
|
142
|
+
rawJSON = body,
|
|
143
|
+
)
|
|
144
|
+
if (result.ok) VerifyOutcome.Passed(result) else VerifyOutcome.Mismatched(result)
|
|
145
|
+
} catch (e: Exception) {
|
|
146
|
+
VerifyOutcome.Error(e.message ?: "Invalid verify response")
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private fun beginDeferredMatchIfNeeded() {
|
|
151
|
+
if (hasDeferredMatchBeenReported) return
|
|
152
|
+
val key = apiKey ?: return
|
|
153
|
+
val ctx = appContext ?: return
|
|
154
|
+
performDeferredMatch(ctx, key, markReported = true)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun verifyInBackground() {
|
|
158
|
+
val key = apiKey ?: return
|
|
159
|
+
val ctx = appContext ?: return
|
|
160
|
+
io.execute {
|
|
161
|
+
val body = JSONObject().apply {
|
|
162
|
+
put("platform", "android")
|
|
163
|
+
put("packageName", CliqItDeviceInfo.packageName(ctx))
|
|
164
|
+
putOpt("androidSha256", CliqItDeviceInfo.androidSha256(ctx))
|
|
165
|
+
}
|
|
166
|
+
val url = CliqItHttp.joinUrl(
|
|
167
|
+
CliqItDefaults.SERVER_URL,
|
|
168
|
+
*CliqItDefaults.VERIFY_PATH.split('/').filter { it.isNotEmpty() }.toTypedArray(),
|
|
169
|
+
)
|
|
170
|
+
CliqItHttp.postJson(url, key, body.toString()).fold(
|
|
171
|
+
onSuccess = { response ->
|
|
172
|
+
lastVerifyJSON = response.body
|
|
173
|
+
val outcome = parseVerifyOutcome(response.body)
|
|
174
|
+
lastVerifyOutcome = outcome
|
|
175
|
+
when (outcome) {
|
|
176
|
+
is VerifyOutcome.Passed ->
|
|
177
|
+
Log.i(TAG, "✅ verify OK — ${outcome.result.appName ?: "app"} (${outcome.result.appId ?: "-"})")
|
|
178
|
+
is VerifyOutcome.Mismatched -> {
|
|
179
|
+
Log.e(TAG, "❌ verify MISMATCH (ok=false)")
|
|
180
|
+
Log.e(TAG, " appName: ${outcome.result.appName ?: "-"}")
|
|
181
|
+
Log.e(TAG, " appId: ${outcome.result.appId ?: "-"}")
|
|
182
|
+
outcome.result.failedChecks.forEach { (field, check) ->
|
|
183
|
+
Log.e(TAG, " • $field: actual=${check.actual ?: "-"} expected=${check.expected ?: "-"}")
|
|
184
|
+
}
|
|
185
|
+
Log.e(TAG, " Fix: use this app's API key, or match package / SHA in admin.")
|
|
186
|
+
}
|
|
187
|
+
is VerifyOutcome.Error ->
|
|
188
|
+
Log.e(TAG, "❌ verify error: ${outcome.message}")
|
|
189
|
+
}
|
|
190
|
+
mainHandler.post { verifyHandler?.invoke(outcome) }
|
|
191
|
+
},
|
|
192
|
+
onFailure = { err ->
|
|
193
|
+
val message = err.message ?: "verify failed"
|
|
194
|
+
Log.e(TAG, "❌ verify error: $message")
|
|
195
|
+
val outcome = VerifyOutcome.Error(message)
|
|
196
|
+
lastVerifyOutcome = outcome
|
|
197
|
+
mainHandler.post { verifyHandler?.invoke(outcome) }
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private fun performDeferredMatch(ctx: Context, key: String, markReported: Boolean) {
|
|
204
|
+
if (!matchInFlight.compareAndSet(false, true)) return
|
|
205
|
+
|
|
206
|
+
io.execute {
|
|
207
|
+
try {
|
|
208
|
+
val body = buildMatchBody(ctx)
|
|
209
|
+
val bodyJson = body.toString()
|
|
210
|
+
val url = CliqItHttp.joinUrl(
|
|
211
|
+
CliqItDefaults.SERVER_URL,
|
|
212
|
+
*CliqItDefaults.DEFERRED_MATCH_PATH.split('/').filter { it.isNotEmpty() }.toTypedArray(),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
CliqItHttp.postJson(url, key, bodyJson).fold(
|
|
216
|
+
onSuccess = { response ->
|
|
217
|
+
val outcome = parseMatchOutcome(response.body)
|
|
218
|
+
lastDeferredOutcome = outcome
|
|
219
|
+
if (markReported && outcome is DeferredMatchOutcome.Matched) {
|
|
220
|
+
prefs()?.edit()?.putBoolean(CliqItDefaults.KEY_MATCH_REPORTED, true)?.apply()
|
|
221
|
+
}
|
|
222
|
+
when (outcome) {
|
|
223
|
+
is DeferredMatchOutcome.Matched ->
|
|
224
|
+
Log.i(TAG, "✅ deferred match OK — path=${outcome.info.destinationPath ?: "-"} slug=${outcome.info.slug ?: "-"}")
|
|
225
|
+
is DeferredMatchOutcome.NotMatched ->
|
|
226
|
+
Log.i(TAG, "✅ deferred match finished — notMatched score=${outcome.info.score}")
|
|
227
|
+
is DeferredMatchOutcome.Failed ->
|
|
228
|
+
Log.e(TAG, "❌ deferred match error: ${outcome.error}")
|
|
229
|
+
}
|
|
230
|
+
mainHandler.post { deferredHandler?.invoke(outcome) }
|
|
231
|
+
if (outcome is DeferredMatchOutcome.Matched) {
|
|
232
|
+
maybeDeliverDeferred(outcome.info)
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
onFailure = { err ->
|
|
236
|
+
val message = err.message ?: "match failed"
|
|
237
|
+
Log.e(TAG, "❌ deferred match error: $message")
|
|
238
|
+
val outcome = DeferredMatchOutcome.Failed(message)
|
|
239
|
+
lastDeferredOutcome = outcome
|
|
240
|
+
mainHandler.post { deferredHandler?.invoke(outcome) }
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
} catch (e: Exception) {
|
|
244
|
+
val message = e.message ?: e.toString()
|
|
245
|
+
Log.e(TAG, "❌ deferred match error: $message", e)
|
|
246
|
+
val outcome = DeferredMatchOutcome.Failed(message)
|
|
247
|
+
lastDeferredOutcome = outcome
|
|
248
|
+
mainHandler.post { deferredHandler?.invoke(outcome) }
|
|
249
|
+
} finally {
|
|
250
|
+
matchInFlight.set(false)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private fun maybeDeliverDeferred(info: DeferredMatchInfo) {
|
|
256
|
+
val prefs = prefs() ?: return
|
|
257
|
+
if (prefs.getBoolean(CliqItDefaults.KEY_DEFERRED_DELIVERED, false)) return
|
|
258
|
+
if (receivedDirectThisSession || pendingPayload != null) return
|
|
259
|
+
val path = LinkDetails.normalizePath(info.destinationPath) ?: return
|
|
260
|
+
prefs.edit().putBoolean(CliqItDefaults.KEY_DEFERRED_DELIVERED, true).apply()
|
|
261
|
+
deliver(
|
|
262
|
+
CliqItPayload(
|
|
263
|
+
url = "${CliqItDefaults.SERVER_URL}$path",
|
|
264
|
+
path = path,
|
|
265
|
+
pathComponents = path.split('/').filter { it.isNotEmpty() },
|
|
266
|
+
query = emptyMap(),
|
|
267
|
+
source = "deferred",
|
|
268
|
+
isDeferred = true,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private fun buildMatchBody(ctx: Context): JSONObject {
|
|
274
|
+
val (level, charging) = CliqItDeviceInfo.battery(ctx)
|
|
275
|
+
// Collect WebView fingerprints before POST (same signals as iOS).
|
|
276
|
+
val web = CliqItWebFingerprint.collect(ctx)
|
|
277
|
+
return JSONObject().apply {
|
|
278
|
+
put("platform", "android")
|
|
279
|
+
put("packageName", CliqItDeviceInfo.packageName(ctx))
|
|
280
|
+
putOpt("androidSha256", CliqItDeviceInfo.androidSha256(ctx))
|
|
281
|
+
put("osVersionMajor", CliqItDeviceInfo.osVersionMajor())
|
|
282
|
+
put("osVersionMajorMinor", CliqItDeviceInfo.osVersionMajorMinor())
|
|
283
|
+
put("deviceModelClass", "android")
|
|
284
|
+
put("deviceName", CliqItDeviceInfo.deviceName())
|
|
285
|
+
put("locale", CliqItDeviceInfo.matchLocale())
|
|
286
|
+
put("timezone", CliqItDeviceInfo.timezone())
|
|
287
|
+
put("screenBucket", CliqItDeviceInfo.screenBucket(ctx))
|
|
288
|
+
put("appOpenAt", System.currentTimeMillis())
|
|
289
|
+
putOpt("connectionType", CliqItDeviceInfo.connectionType(ctx))
|
|
290
|
+
putOpt("batteryLevel", level)
|
|
291
|
+
putOpt("batteryCharging", charging)
|
|
292
|
+
put("languagesOrdered", CliqItDeviceInfo.languagesOrdered())
|
|
293
|
+
put("colorScheme", CliqItDeviceInfo.colorScheme(ctx))
|
|
294
|
+
put("hourCycle", CliqItDeviceInfo.hourCycle())
|
|
295
|
+
putOpt("currency", CliqItDeviceInfo.currencyCode())
|
|
296
|
+
putOpt("regionCode", CliqItDeviceInfo.regionCode())
|
|
297
|
+
put("dynamicTypeSize", CliqItDeviceInfo.fontScale(ctx))
|
|
298
|
+
put("boldText", CliqItDeviceInfo.boldText(ctx))
|
|
299
|
+
put("reduceMotion", CliqItDeviceInfo.reduceMotion(ctx))
|
|
300
|
+
put("increaseContrast", CliqItDeviceInfo.increaseContrast(ctx))
|
|
301
|
+
put("hardwareConcurrency", CliqItDeviceInfo.hardwareConcurrency())
|
|
302
|
+
put("devicePixelRatioBucket", CliqItDeviceInfo.devicePixelRatioBucket(ctx))
|
|
303
|
+
putOpt("clickSessionId", launchClickSessionId)
|
|
304
|
+
web?.clockSkewMs?.let { put("clockSkewMs", it) }
|
|
305
|
+
putOpt("canvasHash", web?.canvasHash)
|
|
306
|
+
putOpt("webglHash", web?.webglHash)
|
|
307
|
+
putOpt("webglVendor", web?.webglVendor)
|
|
308
|
+
putOpt("gpuRenderer", web?.gpuRenderer)
|
|
309
|
+
putOpt("audioFingerprint", web?.audioFingerprint)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private fun parseMatchOutcome(body: String): DeferredMatchOutcome {
|
|
314
|
+
return try {
|
|
315
|
+
val json = JSONObject(body)
|
|
316
|
+
// Some APIs return { status:false, message:"..." } even with HTTP 200
|
|
317
|
+
if (json.has("status") && !json.optBoolean("status", true) && !json.has("matched")) {
|
|
318
|
+
val msg = CliqItHttp.optString(json, "message") ?: body
|
|
319
|
+
return DeferredMatchOutcome.Failed(msg)
|
|
320
|
+
}
|
|
321
|
+
val info = DeferredMatchInfo(
|
|
322
|
+
matched = json.optBoolean("matched", false),
|
|
323
|
+
tier = CliqItHttp.optString(json, "tier"),
|
|
324
|
+
confidence = when {
|
|
325
|
+
json.has("confidence") && !json.isNull("confidence") ->
|
|
326
|
+
json.opt("confidence")?.toString()
|
|
327
|
+
else -> null
|
|
328
|
+
},
|
|
329
|
+
score = CliqItHttp.optDouble(json, "score"),
|
|
330
|
+
destinationPath = CliqItHttp.optString(json, "destinationPath"),
|
|
331
|
+
slug = CliqItHttp.optString(json, "slug"),
|
|
332
|
+
)
|
|
333
|
+
if (info.matched) DeferredMatchOutcome.Matched(info) else DeferredMatchOutcome.NotMatched(info)
|
|
334
|
+
} catch (e: Exception) {
|
|
335
|
+
DeferredMatchOutcome.Failed(e.message ?: "decode failed")
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private fun fetchLinkDetails(
|
|
340
|
+
key: String,
|
|
341
|
+
uri: Uri,
|
|
342
|
+
slug: String,
|
|
343
|
+
fallback: CliqItPayload,
|
|
344
|
+
) {
|
|
345
|
+
val ctx = appContext ?: return
|
|
346
|
+
io.execute {
|
|
347
|
+
var url = CliqItHttp.joinUrl(
|
|
348
|
+
CliqItDefaults.SERVER_URL,
|
|
349
|
+
*CliqItDefaults.LINK_LOOKUP_PATH.split('/').filter { it.isNotEmpty() }.toTypedArray(),
|
|
350
|
+
slug,
|
|
351
|
+
)
|
|
352
|
+
url = CliqItHttp.appendIdentityQuery(url, ctx)
|
|
353
|
+
CliqItHttp.get(url, key).fold(
|
|
354
|
+
onSuccess = { response ->
|
|
355
|
+
when (val parsed = parseLinkDetails(response.body)) {
|
|
356
|
+
is LinkLookupResult.Resolved -> {
|
|
357
|
+
lastLinkDetails = parsed.details
|
|
358
|
+
Log.i(TAG, "✅ link lookup OK — path=${parsed.details.resolvedPath ?: "-"} slug=${parsed.details.slug ?: slug}")
|
|
359
|
+
val resolved = resolvedPayload(parsed.details, uri, fallback)
|
|
360
|
+
mainHandler.post {
|
|
361
|
+
linkLookupHandler?.invoke(parsed)
|
|
362
|
+
deliver(resolved ?: fallback)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
is LinkLookupResult.Failed -> {
|
|
366
|
+
Log.e(TAG, "❌ link lookup error: ${parsed.error}")
|
|
367
|
+
mainHandler.post {
|
|
368
|
+
linkLookupHandler?.invoke(parsed)
|
|
369
|
+
deliver(fallback)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
onFailure = { err ->
|
|
375
|
+
val message = err.message ?: "lookup failed"
|
|
376
|
+
Log.e(TAG, "❌ link lookup error: $message")
|
|
377
|
+
mainHandler.post {
|
|
378
|
+
linkLookupHandler?.invoke(LinkLookupResult.Failed(message))
|
|
379
|
+
deliver(fallback)
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private fun parseLinkDetails(body: String): LinkLookupResult {
|
|
387
|
+
return try {
|
|
388
|
+
val root = JSONObject(body)
|
|
389
|
+
if (!root.optBoolean("status", false)) {
|
|
390
|
+
return LinkLookupResult.Failed(CliqItHttp.optString(root, "message") ?: "Link lookup failed")
|
|
391
|
+
}
|
|
392
|
+
val data = root.optJSONObject("data")
|
|
393
|
+
?: return LinkLookupResult.Failed("Missing data")
|
|
394
|
+
LinkLookupResult.Resolved(
|
|
395
|
+
LinkDetails(
|
|
396
|
+
destination = CliqItHttp.optString(data, "destination"),
|
|
397
|
+
iosDestination = CliqItHttp.optString(data, "iosDestination"),
|
|
398
|
+
androidDestination = CliqItHttp.optString(data, "androidDestination"),
|
|
399
|
+
ogTitle = CliqItHttp.optString(data, "ogTitle"),
|
|
400
|
+
ogDescription = CliqItHttp.optString(data, "ogDescription"),
|
|
401
|
+
ogImage = CliqItHttp.optString(data, "ogImage"),
|
|
402
|
+
ogUrl = CliqItHttp.optString(data, "ogUrl"),
|
|
403
|
+
slug = CliqItHttp.optString(data, "slug"),
|
|
404
|
+
webFallback = CliqItHttp.optString(data, "webFallback"),
|
|
405
|
+
showInterstitial = CliqItHttp.optBool(data, "showInterstitial"),
|
|
406
|
+
isDeepLink = CliqItHttp.optBool(data, "isDeepLink"),
|
|
407
|
+
appleTeamId = CliqItHttp.optString(data, "appleTeamId"),
|
|
408
|
+
iosBundleId = CliqItHttp.optString(data, "iosBundleId"),
|
|
409
|
+
androidPackageName = CliqItHttp.optString(data, "androidPackageName"),
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
} catch (e: Exception) {
|
|
413
|
+
LinkLookupResult.Failed(e.message ?: "Invalid link lookup response")
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private fun resolvedPayload(details: LinkDetails, uri: Uri, fallback: CliqItPayload): CliqItPayload? {
|
|
418
|
+
val path = details.resolvedPath ?: return null
|
|
419
|
+
return CliqItPayload(
|
|
420
|
+
url = uri.toString(),
|
|
421
|
+
path = path,
|
|
422
|
+
pathComponents = path.split('/').filter { it.isNotEmpty() },
|
|
423
|
+
query = fallback.query,
|
|
424
|
+
source = fallback.source,
|
|
425
|
+
isDeferred = false,
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private fun deliver(payload: CliqItPayload) {
|
|
430
|
+
val handler = deepLinkHandler
|
|
431
|
+
if (handler != null) {
|
|
432
|
+
mainHandler.post { handler(payload) }
|
|
433
|
+
} else {
|
|
434
|
+
pendingPayload = payload
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private fun prefs() =
|
|
439
|
+
appContext?.getSharedPreferences(CliqItDefaults.PREFS, Context.MODE_PRIVATE)
|
|
440
|
+
|
|
441
|
+
private fun slugFrom(uri: Uri): String? {
|
|
442
|
+
val last = uri.pathSegments.lastOrNull()?.takeIf { it.isNotEmpty() }
|
|
443
|
+
if (last != null) return last
|
|
444
|
+
return uri.host?.takeIf { it.isNotEmpty() }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private fun captureClickSessionId(uri: Uri) {
|
|
448
|
+
if (launchClickSessionId != null) return
|
|
449
|
+
for (key in listOf("session", "clickSessionId", "click_session_id")) {
|
|
450
|
+
val v = uri.getQueryParameter(key)?.trim()
|
|
451
|
+
if (!v.isNullOrEmpty()) {
|
|
452
|
+
launchClickSessionId = v
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private fun detectSource(uri: Uri): String {
|
|
459
|
+
val scheme = uri.scheme?.lowercase().orEmpty()
|
|
460
|
+
return when {
|
|
461
|
+
scheme == "http" || scheme == "https" -> "universalLink"
|
|
462
|
+
scheme.isNotEmpty() -> "customScheme"
|
|
463
|
+
else -> "unknown"
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Only `*.theblockyapp.com` https hosts, or custom schemes registered by this app. */
|
|
468
|
+
private fun isAllowedUrl(uri: Uri): Boolean {
|
|
469
|
+
val scheme = uri.scheme?.lowercase().orEmpty()
|
|
470
|
+
return when {
|
|
471
|
+
scheme == "http" || scheme == "https" -> {
|
|
472
|
+
val host = uri.host?.lowercase() ?: return false
|
|
473
|
+
isAllowedHost(host)
|
|
474
|
+
}
|
|
475
|
+
scheme.isNotEmpty() -> isRegisteredScheme(scheme)
|
|
476
|
+
else -> false
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private fun isAllowedHost(host: String): Boolean {
|
|
481
|
+
val root = CliqItDefaults.ALLOWED_LINK_DOMAIN.lowercase()
|
|
482
|
+
return host == root || host.endsWith(".$root")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private fun isRegisteredScheme(scheme: String): Boolean {
|
|
486
|
+
val ctx = appContext ?: return false
|
|
487
|
+
return try {
|
|
488
|
+
val probe = android.content.Intent(
|
|
489
|
+
android.content.Intent.ACTION_VIEW,
|
|
490
|
+
Uri.parse("$scheme://cliqit-allow-check"),
|
|
491
|
+
).setPackage(ctx.packageName)
|
|
492
|
+
ctx.packageManager.queryIntentActivities(probe, 0).isNotEmpty()
|
|
493
|
+
} catch (_: Exception) {
|
|
494
|
+
false
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private fun parsePayload(uri: Uri, source: String, isDeferred: Boolean): CliqItPayload {
|
|
499
|
+
val path = uri.path?.takeIf { it.isNotEmpty() } ?: "/"
|
|
500
|
+
val components = uri.pathSegments.filter { it.isNotEmpty() }
|
|
501
|
+
val query = mutableMapOf<String, String>()
|
|
502
|
+
uri.queryParameterNames.forEach { name ->
|
|
503
|
+
uri.getQueryParameter(name)?.let { query[name] = it }
|
|
504
|
+
}
|
|
505
|
+
return CliqItPayload(
|
|
506
|
+
url = uri.toString(),
|
|
507
|
+
path = path,
|
|
508
|
+
pathComponents = components,
|
|
509
|
+
query = query,
|
|
510
|
+
source = source,
|
|
511
|
+
isDeferred = isDeferred,
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
}
|