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,196 @@
|
|
|
1
|
+
package com.cliqit.sdk
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.content.IntentFilter
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.content.res.Configuration
|
|
8
|
+
import android.net.ConnectivityManager
|
|
9
|
+
import android.net.NetworkCapabilities
|
|
10
|
+
import android.os.BatteryManager
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.util.DisplayMetrics
|
|
13
|
+
import android.view.WindowManager
|
|
14
|
+
import java.security.MessageDigest
|
|
15
|
+
import java.util.Currency
|
|
16
|
+
import java.util.Locale
|
|
17
|
+
import java.util.TimeZone
|
|
18
|
+
|
|
19
|
+
internal object CliqItDeviceInfo {
|
|
20
|
+
fun osVersionMajor(): String =
|
|
21
|
+
Build.VERSION.RELEASE.substringBefore('.').ifEmpty { Build.VERSION.SDK_INT.toString() }
|
|
22
|
+
|
|
23
|
+
fun osVersionMajorMinor(): String {
|
|
24
|
+
val parts = Build.VERSION.RELEASE.split('.')
|
|
25
|
+
return when {
|
|
26
|
+
parts.size >= 2 -> "${parts[0]}.${parts[1]}"
|
|
27
|
+
parts.isNotEmpty() -> parts[0]
|
|
28
|
+
else -> Build.VERSION.SDK_INT.toString()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun deviceName(): String = Build.MODEL ?: "unknown"
|
|
33
|
+
|
|
34
|
+
fun matchLocale(): String =
|
|
35
|
+
Locale.getDefault().toLanguageTag().ifEmpty { Locale.getDefault().toString().replace('_', '-') }
|
|
36
|
+
|
|
37
|
+
fun languagesOrdered(): String {
|
|
38
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
39
|
+
val list = android.os.LocaleList.getDefault()
|
|
40
|
+
(0 until list.size()).joinToString(",") { i -> list[i].toLanguageTag() }
|
|
41
|
+
.ifEmpty { Locale.getDefault().toLanguageTag() }
|
|
42
|
+
} else {
|
|
43
|
+
Locale.getDefault().toLanguageTag()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun boldText(context: Context): Boolean {
|
|
48
|
+
return try {
|
|
49
|
+
android.provider.Settings.Secure.getInt(
|
|
50
|
+
context.contentResolver,
|
|
51
|
+
"accessibility_bold_text",
|
|
52
|
+
0,
|
|
53
|
+
) == 1
|
|
54
|
+
} catch (_: Exception) {
|
|
55
|
+
false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun reduceMotion(context: Context): Boolean {
|
|
60
|
+
return try {
|
|
61
|
+
val scale = android.provider.Settings.Global.getFloat(
|
|
62
|
+
context.contentResolver,
|
|
63
|
+
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
|
|
64
|
+
1f,
|
|
65
|
+
)
|
|
66
|
+
scale == 0f
|
|
67
|
+
} catch (_: Exception) {
|
|
68
|
+
false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun increaseContrast(context: Context): Boolean {
|
|
73
|
+
return try {
|
|
74
|
+
android.provider.Settings.Secure.getInt(
|
|
75
|
+
context.contentResolver,
|
|
76
|
+
"high_text_contrast_enabled",
|
|
77
|
+
0,
|
|
78
|
+
) == 1
|
|
79
|
+
} catch (_: Exception) {
|
|
80
|
+
false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun timezone(): String = TimeZone.getDefault().id
|
|
85
|
+
|
|
86
|
+
fun screenBucket(context: Context): String {
|
|
87
|
+
val metrics = DisplayMetrics()
|
|
88
|
+
@Suppress("DEPRECATION")
|
|
89
|
+
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(metrics)
|
|
90
|
+
val minSide = minOf(metrics.widthPixels, metrics.heightPixels).toFloat() / metrics.density
|
|
91
|
+
val maxSide = maxOf(metrics.widthPixels, metrics.heightPixels).toFloat() / metrics.density
|
|
92
|
+
return when {
|
|
93
|
+
minSide >= 600 -> "tablet"
|
|
94
|
+
maxSide >= 800 -> "large"
|
|
95
|
+
maxSide >= 700 -> "medium"
|
|
96
|
+
else -> "small"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun colorScheme(context: Context): String {
|
|
101
|
+
val night = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
102
|
+
return if (night == Configuration.UI_MODE_NIGHT_YES) "dark" else "light"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fun hourCycle(): String {
|
|
106
|
+
val pattern = android.text.format.DateFormat.getBestDateTimePattern(Locale.getDefault(), "j")
|
|
107
|
+
return if (pattern.contains('a')) "h12" else "h24"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fun currencyCode(): String? = try {
|
|
111
|
+
Currency.getInstance(Locale.getDefault()).currencyCode
|
|
112
|
+
} catch (_: Exception) {
|
|
113
|
+
null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fun regionCode(): String? = Locale.getDefault().country.takeIf { it.isNotEmpty() }
|
|
117
|
+
|
|
118
|
+
/// Matches iOS / web: `"high"` (≥2×) or `"standard"`.
|
|
119
|
+
fun devicePixelRatioBucket(context: Context): String {
|
|
120
|
+
val density = context.resources.displayMetrics.density
|
|
121
|
+
return if (density >= 2.0f) "high" else "standard"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun battery(context: Context): Pair<Double?, Boolean?> {
|
|
125
|
+
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
|
126
|
+
?: return null to null
|
|
127
|
+
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
|
128
|
+
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
|
129
|
+
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
|
130
|
+
val pct = if (level >= 0 && scale > 0) level.toDouble() / scale.toDouble() else null
|
|
131
|
+
val charging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
|
132
|
+
status == BatteryManager.BATTERY_STATUS_FULL
|
|
133
|
+
return pct to charging
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fun connectionType(context: Context): String? {
|
|
137
|
+
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return null
|
|
138
|
+
val network = cm.activeNetwork ?: return null
|
|
139
|
+
val caps = cm.getNetworkCapabilities(network) ?: return null
|
|
140
|
+
return when {
|
|
141
|
+
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
|
|
142
|
+
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
|
|
143
|
+
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
|
|
144
|
+
else -> "other"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun hardwareConcurrency(): Int = Runtime.getRuntime().availableProcessors()
|
|
149
|
+
|
|
150
|
+
fun fontScale(context: Context): String {
|
|
151
|
+
val scale = context.resources.configuration.fontScale
|
|
152
|
+
return when {
|
|
153
|
+
scale <= 0.85f -> "S"
|
|
154
|
+
scale <= 1.0f -> "M"
|
|
155
|
+
scale <= 1.15f -> "L"
|
|
156
|
+
scale <= 1.3f -> "XL"
|
|
157
|
+
else -> "XXL"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fun packageName(context: Context): String = context.packageName
|
|
162
|
+
|
|
163
|
+
/** Signing cert SHA-256, colon-separated uppercase (same style as keytool / assetlinks). */
|
|
164
|
+
fun androidSha256(context: Context): String? {
|
|
165
|
+
return try {
|
|
166
|
+
val pm = context.packageManager
|
|
167
|
+
val packageName = context.packageName
|
|
168
|
+
val signatures: Array<android.content.pm.Signature> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
169
|
+
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
170
|
+
pm.getPackageInfo(
|
|
171
|
+
packageName,
|
|
172
|
+
PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong()),
|
|
173
|
+
)
|
|
174
|
+
} else {
|
|
175
|
+
@Suppress("DEPRECATION")
|
|
176
|
+
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
|
|
177
|
+
}
|
|
178
|
+
val signingInfo = info.signingInfo ?: return null
|
|
179
|
+
if (signingInfo.hasMultipleSigners()) {
|
|
180
|
+
signingInfo.apkContentsSigners
|
|
181
|
+
} else {
|
|
182
|
+
signingInfo.signingCertificateHistory
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
@Suppress("DEPRECATION")
|
|
186
|
+
pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures
|
|
187
|
+
} ?: return null
|
|
188
|
+
|
|
189
|
+
val cert = signatures.firstOrNull()?.toByteArray() ?: return null
|
|
190
|
+
val digest = MessageDigest.getInstance("SHA-256").digest(cert)
|
|
191
|
+
digest.joinToString(":") { b -> "%02X".format(b) }
|
|
192
|
+
} catch (e: Exception) {
|
|
193
|
+
null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package com.cliqit.sdk
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
import java.io.BufferedReader
|
|
5
|
+
import java.io.InputStreamReader
|
|
6
|
+
import java.io.OutputStreamWriter
|
|
7
|
+
import java.net.HttpURLConnection
|
|
8
|
+
import java.net.URL
|
|
9
|
+
|
|
10
|
+
internal object CliqItHttp {
|
|
11
|
+
data class Response(val statusCode: Int, val body: String)
|
|
12
|
+
|
|
13
|
+
fun get(url: String, apiKey: String): Result<Response> =
|
|
14
|
+
request(url, "GET", apiKey, null)
|
|
15
|
+
|
|
16
|
+
fun postJson(url: String, apiKey: String, jsonBody: String): Result<Response> =
|
|
17
|
+
request(url, "POST", apiKey, jsonBody)
|
|
18
|
+
|
|
19
|
+
private fun request(
|
|
20
|
+
url: String,
|
|
21
|
+
method: String,
|
|
22
|
+
apiKey: String,
|
|
23
|
+
jsonBody: String?,
|
|
24
|
+
): Result<Response> {
|
|
25
|
+
var lastError = "Request failed"
|
|
26
|
+
repeat(CliqItDefaults.MAX_RETRY) { attempt ->
|
|
27
|
+
if (attempt > 0) {
|
|
28
|
+
Thread.sleep(CliqItDefaults.RETRY_INITIAL_DELAY_MS * (1L shl (attempt - 1)))
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
val conn = (URL(url).openConnection() as HttpURLConnection).apply {
|
|
32
|
+
requestMethod = method
|
|
33
|
+
connectTimeout = 15_000
|
|
34
|
+
readTimeout = 20_000
|
|
35
|
+
setRequestProperty(CliqItDefaults.API_KEY_HEADER, apiKey)
|
|
36
|
+
setRequestProperty("Accept", "application/json")
|
|
37
|
+
if (jsonBody != null) {
|
|
38
|
+
doOutput = true
|
|
39
|
+
setRequestProperty("Content-Type", "application/json")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (jsonBody != null) {
|
|
43
|
+
OutputStreamWriter(conn.outputStream, Charsets.UTF_8).use { it.write(jsonBody) }
|
|
44
|
+
}
|
|
45
|
+
val code = conn.responseCode
|
|
46
|
+
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
|
|
47
|
+
val body = stream?.let { BufferedReader(InputStreamReader(it, Charsets.UTF_8)).readText() }.orEmpty()
|
|
48
|
+
conn.disconnect()
|
|
49
|
+
|
|
50
|
+
if (code in 200..299) {
|
|
51
|
+
return Result.success(Response(code, body))
|
|
52
|
+
}
|
|
53
|
+
lastError = "Request failed ($code): ${body.ifEmpty { "empty" }}"
|
|
54
|
+
if (code == 408 || code == 429 || code in 500..599) {
|
|
55
|
+
// retry
|
|
56
|
+
} else {
|
|
57
|
+
return Result.failure(Exception(lastError))
|
|
58
|
+
}
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
lastError = e.message ?: "Request failed"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return Result.failure(Exception(lastError))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun joinUrl(base: String, vararg parts: String): String {
|
|
67
|
+
val trimmedBase = base.trimEnd('/')
|
|
68
|
+
val path = parts.joinToString("/") { it.trim('/') }
|
|
69
|
+
return "$trimmedBase/$path"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Append platform identity query params (same fields as verify / match body). */
|
|
73
|
+
fun appendIdentityQuery(url: String, context: android.content.Context): String {
|
|
74
|
+
val sep = if (url.contains('?')) '&' else '?'
|
|
75
|
+
val platform = "platform=android"
|
|
76
|
+
val packageName = "packageName=${java.net.URLEncoder.encode(CliqItDeviceInfo.packageName(context), Charsets.UTF_8.name())}"
|
|
77
|
+
val sha = CliqItDeviceInfo.androidSha256(context)?.let {
|
|
78
|
+
"&androidSha256=${java.net.URLEncoder.encode(it, Charsets.UTF_8.name())}"
|
|
79
|
+
}.orEmpty()
|
|
80
|
+
return "$url$sep$platform&$packageName$sha"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fun optString(obj: JSONObject?, key: String): String? {
|
|
84
|
+
if (obj == null || !obj.has(key) || obj.isNull(key)) return null
|
|
85
|
+
val v = obj.optString(key, "")
|
|
86
|
+
return v.takeIf { it.isNotEmpty() && !it.equals("null", ignoreCase = true) }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fun optBool(obj: JSONObject?, key: String): Boolean? {
|
|
90
|
+
if (obj == null || !obj.has(key) || obj.isNull(key)) return null
|
|
91
|
+
return obj.optBoolean(key)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fun optDouble(obj: JSONObject?, key: String): Double? {
|
|
95
|
+
if (obj == null || !obj.has(key) || obj.isNull(key)) return null
|
|
96
|
+
return obj.optDouble(key).takeUnless { it.isNaN() }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
package com.cliqit.sdk
|
|
2
|
+
|
|
3
|
+
/** Mirrors iOS `CliqItLinkField`. */
|
|
4
|
+
object LinkField {
|
|
5
|
+
const val destination = "destination"
|
|
6
|
+
const val iosDestination = "iosDestination"
|
|
7
|
+
const val androidDestination = "androidDestination"
|
|
8
|
+
const val ogTitle = "ogTitle"
|
|
9
|
+
const val ogDescription = "ogDescription"
|
|
10
|
+
const val ogImage = "ogImage"
|
|
11
|
+
const val ogUrl = "ogUrl"
|
|
12
|
+
const val slug = "slug"
|
|
13
|
+
const val webFallback = "webFallback"
|
|
14
|
+
const val showInterstitial = "showInterstitial"
|
|
15
|
+
const val isDeepLink = "isDeepLink"
|
|
16
|
+
const val appleTeamId = "appleTeamId"
|
|
17
|
+
const val iosBundleId = "iosBundleId"
|
|
18
|
+
const val androidPackageName = "androidPackageName"
|
|
19
|
+
const val resolvedPath = "resolvedPath"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
data class CliqItPayload(
|
|
23
|
+
val url: String,
|
|
24
|
+
val path: String,
|
|
25
|
+
val pathComponents: List<String>,
|
|
26
|
+
val query: Map<String, String>,
|
|
27
|
+
val source: String,
|
|
28
|
+
val isDeferred: Boolean,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
data class DeferredMatchInfo(
|
|
32
|
+
val matched: Boolean,
|
|
33
|
+
val tier: String?,
|
|
34
|
+
val confidence: String?,
|
|
35
|
+
val score: Double?,
|
|
36
|
+
val destinationPath: String?,
|
|
37
|
+
val slug: String?,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
sealed class DeferredMatchOutcome {
|
|
41
|
+
data class Matched(val info: DeferredMatchInfo) : DeferredMatchOutcome()
|
|
42
|
+
data class NotMatched(val info: DeferredMatchInfo) : DeferredMatchOutcome()
|
|
43
|
+
data class Failed(val error: String) : DeferredMatchOutcome()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
data class LinkDetails(
|
|
47
|
+
val destination: String?,
|
|
48
|
+
val iosDestination: String?,
|
|
49
|
+
val androidDestination: String?,
|
|
50
|
+
val ogTitle: String?,
|
|
51
|
+
val ogDescription: String?,
|
|
52
|
+
val ogImage: String?,
|
|
53
|
+
val ogUrl: String?,
|
|
54
|
+
val slug: String?,
|
|
55
|
+
val webFallback: String?,
|
|
56
|
+
val showInterstitial: Boolean?,
|
|
57
|
+
val isDeepLink: Boolean?,
|
|
58
|
+
val appleTeamId: String?,
|
|
59
|
+
val iosBundleId: String?,
|
|
60
|
+
val androidPackageName: String?,
|
|
61
|
+
) {
|
|
62
|
+
/** Android: `androidDestination` ?? `destination`. */
|
|
63
|
+
val resolvedPath: String?
|
|
64
|
+
get() = normalizePath(androidDestination) ?: normalizePath(destination)
|
|
65
|
+
|
|
66
|
+
fun field(key: String): String? = when (key) {
|
|
67
|
+
LinkField.destination -> destination
|
|
68
|
+
LinkField.iosDestination -> iosDestination
|
|
69
|
+
LinkField.androidDestination -> androidDestination
|
|
70
|
+
LinkField.ogTitle -> ogTitle
|
|
71
|
+
LinkField.ogDescription -> ogDescription
|
|
72
|
+
LinkField.ogImage -> ogImage
|
|
73
|
+
LinkField.ogUrl -> ogUrl
|
|
74
|
+
LinkField.slug -> slug
|
|
75
|
+
LinkField.webFallback -> webFallback
|
|
76
|
+
LinkField.showInterstitial -> showInterstitial?.toString()
|
|
77
|
+
LinkField.isDeepLink -> isDeepLink?.toString()
|
|
78
|
+
LinkField.appleTeamId -> appleTeamId
|
|
79
|
+
LinkField.iosBundleId -> iosBundleId
|
|
80
|
+
LinkField.androidPackageName -> androidPackageName
|
|
81
|
+
LinkField.resolvedPath -> resolvedPath
|
|
82
|
+
else -> null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
companion object {
|
|
86
|
+
fun normalizePath(raw: String?): String? {
|
|
87
|
+
var value = raw?.trim().orEmpty()
|
|
88
|
+
if (value.isEmpty() || value.equals("null", ignoreCase = true)) return null
|
|
89
|
+
if (!value.startsWith("/")) value = "/$value"
|
|
90
|
+
return value
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
sealed class VerifyOutcome {
|
|
96
|
+
data class Passed(val result: VerifyResult) : VerifyOutcome()
|
|
97
|
+
data class Mismatched(val result: VerifyResult) : VerifyOutcome()
|
|
98
|
+
data class Error(val message: String) : VerifyOutcome()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
data class VerifyCheck(
|
|
102
|
+
val actual: String?,
|
|
103
|
+
val expected: String?,
|
|
104
|
+
val match: Boolean,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
data class VerifyResult(
|
|
108
|
+
val ok: Boolean,
|
|
109
|
+
val appId: String?,
|
|
110
|
+
val appName: String?,
|
|
111
|
+
val checks: Map<String, VerifyCheck>,
|
|
112
|
+
val rawJSON: String,
|
|
113
|
+
) {
|
|
114
|
+
val failedChecks: List<Pair<String, VerifyCheck>>
|
|
115
|
+
get() = checks.filter { !it.value.match }.toList().sortedBy { it.first }
|
|
116
|
+
|
|
117
|
+
val mismatchMessage: String
|
|
118
|
+
get() {
|
|
119
|
+
if (ok) return "Verify OK"
|
|
120
|
+
if (failedChecks.isEmpty()) return "Verify failed (ok=false)"
|
|
121
|
+
return failedChecks.joinToString("; ") { (field, check) ->
|
|
122
|
+
"$field: actual=${check.actual ?: "-"} expected=${check.expected ?: "-"}"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
sealed class LinkLookupResult {
|
|
128
|
+
data class Resolved(val details: LinkDetails) : LinkLookupResult()
|
|
129
|
+
data class Failed(val error: String) : LinkLookupResult()
|
|
130
|
+
}
|