-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathllms.txt
More file actions
492 lines (397 loc) · 14.9 KB
/
Copy pathllms.txt
File metadata and controls
492 lines (397 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# com.linkforty:sdk (Android)
> Native Kotlin SDK for LinkForty — an open-source mobile attribution and deep link management platform. Handles App Links, deferred deep linking (install attribution), in-app event tracking with offline queue, revenue tracking, and link creation. Zero external dependencies beyond Kotlin stdlib. Works with LinkForty Cloud (linkforty.com) or self-hosted Core instances.
## Requirements
- Android API 26+ (Android 8.0 Oreo)
- Kotlin 1.9+
- JDK 17
## Installation
### build.gradle.kts (app level)
```kotlin
dependencies {
implementation("com.linkforty:sdk:1.1.0")
}
```
### settings.gradle.kts (if using GitHub Packages)
```kotlin
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/linkforty/mobile-sdk-android")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_USER")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}
}
```
### build.gradle (Groovy)
```groovy
dependencies {
implementation 'com.linkforty:sdk:1.1.0'
}
```
## Kotlin Types
```kotlin
data class LinkFortyConfig(
val baseURL: String,
val apiKey: String? = null, // only needed for createLink()
val appToken: String? = null, // Cloud only — recommended; enables organic-install attribution
val debug: Boolean = false,
val attributionWindowHours: Int = 168 // 7 days, range: 1-2160
)
data class DeepLinkData(
val shortCode: String,
val iosURL: String? = null,
val androidURL: String? = null,
val webURL: String? = null,
val utmParameters: UTMParameters? = null,
/** Custom key-value parameters set when the link was created */
val customParameters: Map<String, String>? = null,
/** In-app destination path (e.g., "/product/123") */
val deepLinkPath: String? = null,
/** Custom URI scheme (e.g., "myapp") */
val appScheme: String? = null,
val clickedAt: String? = null,
val linkId: String? = null
) {
/** Parse clickedAt as an Instant */
fun clickedAtDate(): Instant?
}
data class UTMParameters(
val source: String? = null,
val medium: String? = null,
val campaign: String? = null,
val term: String? = null,
val content: String? = null
)
data class InstallResponse(
val installId: String,
val attributed: Boolean,
/** Attribution confidence score (0-100) */
val confidenceScore: Int,
/** Matched fingerprint factors (e.g., ["ip", "user_agent", "timezone"]) */
val matchedFactors: List<String>,
val deepLinkData: DeepLinkData?
)
data class CreateLinkOptions(
val templateId: String? = null, // auto-selected when null
val templateSlug: String? = null,
val deepLinkParameters: Map<String, String>? = null,
val title: String? = null,
val description: String? = null,
val customCode: String? = null,
val utmParameters: UTMParameters? = null
)
data class CreateLinkResult(
val url: String, // full shareable URL
val shortCode: String,
val linkId: String
)
/** All errors thrown by the SDK */
sealed class LinkFortyError(message: String, cause: Throwable? = null) : Exception(message, cause) {
class NotInitialized : LinkFortyError("SDK not initialized")
class AlreadyInitialized : LinkFortyError("SDK already initialized")
class InvalidConfiguration(detail: String) : LinkFortyError(detail)
class NetworkError(cause: Throwable) : LinkFortyError("Network error", cause)
class InvalidResponse(statusCode: Int?, responseMessage: String?) : LinkFortyError("...")
class DecodingError(cause: Throwable) : LinkFortyError("Decoding error", cause)
class EncodingError(cause: Throwable) : LinkFortyError("Encoding error", cause)
class InvalidEventData(detail: String) : LinkFortyError(detail)
class InvalidDeepLinkUrl(detail: String) : LinkFortyError(detail)
class MissingApiKey : LinkFortyError("API key required for this operation")
}
```
## SDK API
Initialize with the static `LinkForty.initialize()` method, then use `LinkForty.shared` for all other calls. All async methods are Kotlin `suspend` functions — call from a coroutine scope.
```kotlin
import com.linkforty.sdk.LinkForty
import com.linkforty.sdk.models.LinkFortyConfig
```
### LinkForty.initialize(context, config): InstallResponse (suspend)
Initialize the SDK, report the install, and return attribution data. Must be called before any other method.
```kotlin
val config = LinkFortyConfig(
baseURL = "https://go.yourdomain.com",
apiKey = "lf_live_abc123", // optional — only for createLink()
appToken = "at_a1b2c3d4...", // recommended for Cloud — enables organic-install attribution
debug = BuildConfig.DEBUG
)
lifecycleScope.launch {
val response = LinkForty.initialize(
context = applicationContext,
config = config
)
Log.d("LinkForty", "Install ID: ${response.installId}")
Log.d("LinkForty", "Attributed: ${response.attributed}")
Log.d("LinkForty", "Confidence: ${response.confidenceScore}")
}
```
### onDeepLink(callback)
Register a callback for direct deep links — fires when user taps an App Link and the app is already installed. Callbacks execute on the main thread.
```kotlin
LinkForty.shared.onDeepLink { uri, data ->
Log.d("LinkForty", "Deep link opened: $uri")
data?.customParameters?.let { params ->
val route = params["route"]
val id = params["id"]
if (route != null && id != null) {
navigateToContent(route, id)
}
}
}
```
### onDeferredDeepLink(callback)
Register a callback for deferred deep links — fires on first app launch if the install was attributed to a link click.
```kotlin
LinkForty.shared.onDeferredDeepLink { data ->
data?.let {
Log.d("LinkForty", "Attributed install from: ${it.shortCode}")
it.customParameters?.let { params ->
val route = params["route"]
val id = params["id"]
if (route != null && id != null) {
navigateToContent(route, id)
}
}
}
}
```
### handleDeepLink(uri)
Forward App Link intents to the SDK. Call from `onCreate()` and `onNewIntent()`.
```kotlin
intent.data?.let { uri ->
LinkForty.shared.handleDeepLink(uri)
}
```
### trackEvent(name, properties) (suspend)
Track in-app events. Failed sends are queued offline (max 100 events) and retried automatically.
```kotlin
lifecycleScope.launch {
LinkForty.shared.trackEvent(
name = "add_to_cart",
properties = mapOf("productId" to "789", "price" to 29.99)
)
}
```
### trackRevenue(amount, currency, properties) (suspend)
Track revenue events. Uses `BigDecimal` for precision.
```kotlin
lifecycleScope.launch {
LinkForty.shared.trackRevenue(
amount = BigDecimal("29.99"),
currency = "USD",
properties = mapOf("productId" to "789", "orderId" to "order_123")
)
}
```
### createLink(options): CreateLinkResult (suspend)
Create a shareable link. Requires `apiKey` in config.
```kotlin
lifecycleScope.launch {
val result = LinkForty.shared.createLink(
options = CreateLinkOptions(
deepLinkParameters = mapOf("route" to "product", "productId" to "789"),
title = "Check out this product!",
utmParameters = UTMParameters(source = "in_app_share", medium = "referral")
)
)
Log.d("LinkForty", "URL: ${result.url}") // https://go.yourdomain.com/tmpl/abc12345
Log.d("LinkForty", "Code: ${result.shortCode}") // abc12345
Log.d("LinkForty", "ID: ${result.linkId}") // uuid
}
```
### Offline Event Queue
```kotlin
LinkForty.shared.queuedEventCount // number of events waiting
lifecycleScope.launch { LinkForty.shared.flushEvents() } // manually flush
LinkForty.shared.clearEventQueue() // clear without sending
```
### Utility Methods
```kotlin
LinkForty.shared.getInstallId() // String?
LinkForty.shared.getInstallData() // DeepLinkData?
LinkForty.shared.isFirstLaunch() // Boolean
LinkForty.shared.clearData() // clear all stored data
LinkForty.shared.reset() // reset to uninitialized state
```
## App Links Setup
### 1. AndroidManifest.xml
Add an intent filter with `android:autoVerify="true"` to your main activity:
```xml
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- LinkForty App Links -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="go.yourdomain.com" />
</intent-filter>
</activity>
```
`android:launchMode="singleTask"` is required so `onNewIntent()` fires when the app is already running.
### 2. SHA-256 Fingerprint
Configure your app's signing certificate fingerprint in the LinkForty dashboard (Settings → App Configuration).
```bash
# Debug keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
# Release keystore
keytool -list -v -keystore your-release.keystore -alias your-alias
```
### 3. Digital Asset Links
LinkForty automatically serves `/.well-known/assetlinks.json` from your domain. No manual file hosting needed.
## Complete Activity Example
```kotlin
// MainActivity.kt
package com.example.myapp
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.linkforty.sdk.LinkForty
import com.linkforty.sdk.models.LinkFortyConfig
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
val config = LinkFortyConfig(
baseURL = "https://go.yourdomain.com",
apiKey = "lf_live_abc123",
appToken = "at_a1b2c3d4...",
debug = BuildConfig.DEBUG
)
val response = LinkForty.initialize(
context = applicationContext,
config = config
)
LinkForty.shared.onDeferredDeepLink { data ->
data?.let { routeToContent(it) }
}
LinkForty.shared.onDeepLink { uri, data ->
data?.let { routeToContent(it) }
}
if (response.attributed) {
Log.d("LinkForty", "Attributed! Confidence: ${response.confidenceScore}")
}
}
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
intent.data?.let { uri ->
LinkForty.shared.handleDeepLink(uri)
}
}
private fun routeToContent(data: com.linkforty.sdk.models.DeepLinkData) {
val route = data.customParameters?.get("route") ?: return
val id = data.customParameters?.get("id") ?: return
runOnUiThread {
when (route) {
"product" -> startActivity(
Intent(this, ProductActivity::class.java).putExtra("id", id)
)
"profile" -> startActivity(
Intent(this, ProfileActivity::class.java).putExtra("id", id)
)
}
}
}
}
```
## Complete Jetpack Compose Example
```kotlin
// MainActivity.kt
package com.example.myapp
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.linkforty.sdk.LinkForty
import com.linkforty.sdk.models.LinkFortyConfig
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val config = LinkFortyConfig(
baseURL = "https://go.yourdomain.com",
apiKey = "lf_live_abc123",
appToken = "at_a1b2c3d4...",
debug = BuildConfig.DEBUG
)
LinkForty.initialize(context = applicationContext, config = config)
}
intent.data?.let { LinkForty.shared.handleDeepLink(it) }
setContent {
val navController = rememberNavController()
LaunchedEffect(Unit) {
LinkForty.shared.onDeepLink { _, data ->
data?.customParameters?.let { params ->
val route = params["route"]
val id = params["id"]
if (route != null && id != null) {
navController.navigate("$route/$id")
}
}
}
LinkForty.shared.onDeferredDeepLink { data ->
data?.customParameters?.let { params ->
val route = params["route"]
val id = params["id"]
if (route != null && id != null) {
navController.navigate("$route/$id")
}
}
}
}
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen() }
composable("product/{id}") { backStackEntry ->
ProductScreen(id = backStackEntry.arguments?.getString("id") ?: "")
}
composable("profile/{id}") { backStackEntry ->
ProfileScreen(id = backStackEntry.arguments?.getString("id") ?: "")
}
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let { LinkForty.shared.handleDeepLink(it) }
}
}
```
## Testing Deep Links
```bash
# Android Emulator
adb shell am start -a android.intent.action.VIEW -d "https://go.yourdomain.com/abc123"
```
## Self-Hosted vs Cloud
The only change is the `baseURL`:
```kotlin
// Cloud (managed SaaS)
val config = LinkFortyConfig(baseURL = "https://go.linkforty.com")
// Self-hosted (@linkforty/core)
val config = LinkFortyConfig(baseURL = "https://links.yourdomain.com")
```
All SDK methods work identically with both.