SDK Kotlin

Lihat di GitHub

Kotlin Multiplatform, menyasar Android dan iOS dari satu sumber. Versi 0.1.0, mode OAuth saja di Tahap 1. Butuh Android minSdk 24, iOS 15, dan Kotlin 2.3.

Pasang

Belum terbit di Maven Central. Koordinat di bawah adalah yang berlaku begitu rilis 0.1.0 terbit.
// build.gradle.kts
dependencies {
  implementation("id.tracive:tracive-sdk:0.1.0")
}

Membuat client

Satu instance per proses, bukan per layar: ia memegang HTTP client, token store, dan handle database. Pakai konstanta TraciveScopes, bukan string: server menolak seluruh permintaan consent kalau satu nama scope tidak dikenalnya.

object Tracive {
  val client: TraciveClient by lazy {
    TraciveClient(
      mode = TraciveMode.OAuth(
        clientId = "trcvc_…",                                    // dari portal
        redirectUri = "https://app-anda.example/oauth/callback",   // sama persis dengan yang terdaftar
        scopes = listOf(TraciveScopes.SCAMMER_CHECK, TraciveScopes.PROFILE_READ),
        partnerUserId = customerId,                               // id Anda sendiri, buram dan stabil
        clientType = ClientType.PUBLIC,                           // aplikasi menukar code sendiri
      ),
      environment = TraciveEnvironment.Sandbox,                   // Production adalah bawaannya
      cache = checkCache,                                         // tanpa ini cache mati; lihat Cache
    )
  }
}
Tidak ada parameter API key di seluruh permukaan SDK ini, di mode mana pun. Itu invariant desain, bukan kelalaian: kunci yang dikirim di dalam APK adalah kunci yang sudah dipublikasikan.

Setup Android

Activity yang menerima redirect. Pakai App Link https di domain milik Anda, dengan /.well-known/assetlinks.json yang menyebut package dan sidik jari sertifikat Anda: skema kustom bisa diklaim aplikasi lain di perangkat yang sama.

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">   <!-- redirect tiba di onNewIntent -->
  <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="app-anda.example" android:path="/oauth/callback" />
  </intent-filter>
</activity>

Lalu serahkan redirect ke SDK dari dua tempat. Selama consent aplikasi Anda ada di latar belakang dan Android bisa mematikannya; redirect lalu memulai ulang aplikasi lewat onCreate, bukan onNewIntent, dan di situlah grant tiba.

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    intent?.data?.let { deliver(it.toString()) }   // cold start yang dipicu redirect
  }

  override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    intent.data?.let { deliver(it.toString()) }
  }

  private fun deliver(url: String) {
    intent.data = null   // rotasi layar tidak boleh memutar ulang redirect yang sudah terpakai
    lifecycleScope.launch {
      when (val auth = Tracive.client.oauth?.handleCallback(url)) {
        null -> Unit                                       // bukan redirect Tracive
        is AuthResult.Authorized -> onConnected(auth.grantedScopes)
        is AuthResult.CodeReceived -> sendToOwnBackend(auth.code, auth.codeVerifier, auth.redirectUri)
        is AuthResult.Denied -> respectThat()
        is AuthResult.Failed -> report(auth.error)
        is AuthResult.AppNotInstalled -> Unit              // tidak pernah muncul di sini
      }
    }
  }
}
  • · Blok <queries> untuk package visibility sudah ikut di dalam AAR, untuk sandbox maupun production. Jangan tambahkan sendiri dan jangan hapus: tanpanya, pemeriksaan “aplikasi Tracive terpasang?” menjawab tidak untuk setiap pengguna.
  • · null dari handleCallback berarti URL itu bukan redirect Tracive, jadi teruskan ke penanganan deep link Anda yang lain.
Jangan pernah membuka URL authorize di WebView. App Link tidak menyala dari WebView, jadi redirect-nya tidak akan pernah kembali, dan RFC 8252 §8.12 memang melarangnya. SDK memakai Custom Tabs.

Setup iOS

Daftarkan kedua skema Tracive di Info.plist. Tanpanya canOpenURL menjawab tidak, dan setiap pengguna yang sudah memasang Tracive dikirim ke App Store. tracivesandbox adalah build sandbox; lupa yang ini hanya merusak sandbox, jadi lebih sulit ketahuan.

<key>LSApplicationQueriesSchemes</key>
<array>
  <string>tracive</string>
  <string>tracivesandbox</string>
</array>

Redirect-nya sebaiknya Universal Link (Associated Domains plus apple-app-site-association). Serahkan URL yang masuk ke instance client yang sama yang diekspos modul bersama Anda:

// SwiftUI. handleCallback di Kotlin adalah suspend, jadi di Swift ia async.
.onOpenURL { url in
  Task {
    let auth = try? await SharedClient.shared.client.oauth?.handleCallback(url: url.absoluteString)
    switch auth {
    case nil: break                                          // bukan milik Tracive
    case let a as AuthResult.Authorized: onConnected(a.grantedScopes)
    case is AuthResult.Denied: respectThat()
    case let f as AuthResult.Failed: report(f.error)
    default: break
    }
  }
}

Otorisasi

authorize() membuka layar consent lalu selesai. Ia tidak pernah mengembalikan grant: grant tiba lewat handleCallback yang sudah Anda pasang di atas, karena redirect bisa tiba di proses yang berbeda. Satu tempat untuk menangani akhir otorisasi, bukan dua.

// Membuka layar consent Tracive lalu selesai. Hasil sebenarnya tiba lewat handleCallback.
when (val start = Tracive.client.oauth!!.authorize()) {
  null -> showWaitingForConsent()                     // halaman terbuka, tunggu redirect
  is AuthResult.AppNotInstalled -> showInstallHint()  // halaman install sudah dibuka
  is AuthResult.Failed -> report(start.error)         // tidak ada yang terbuka
  else -> Unit
}

AppNotInstalled berarti SDK sudah membuka halaman install. Tidak ada yang dilanjutkan otomatis setelah pengguna kembali; minta mereka memasang Tracive lalu mencoba lagi.

Mengecek

TraciveQuery.phone hanya menerima E.164, jadi 081234567891 ditolak alih-alih ditebak. normalizePhoneNumber menambahkan kode negaranya; argumen pertama berupa kode alpha-3 (IDN) atau kode telepon (+62). Builder lainnya: email, bankAccount, iban, crypto, dan username. Untuk menyebut bank dengan kode nasional atau BIC, pakai bankAccountByShortCode atau bankAccountByBic. Kode yang tidak ada di daftar kode bank ditolak di perangkat dengan UNKNOWN_BANK_CODE, jadi tidak pernah ditagih.

val query = try {
  TraciveQuery.phone(normalizePhoneNumber("IDN", input))   // "0857-1860-9079" → +6285718609079
} catch (e: TraciveQueryException) {
  return showInputError(e.reason)                          // tidak ada yang terkirim, tidak ditagih
}

val result = Tracive.client.checkScammer(query)
render(result.riskTier, result.reportTotal, result.disclaimer)   // disclaimer wajib tampil

Semua builder melipat identifier di klien dengan aturan yang sama dengan server, dan menolak input yang mustahil cocok sebelum ada yang terkirim, karena miss tetap ditagih. e.reason adalah token stabil yang sama di keempat SDK dan tidak pernah memuat input yang ditolak. Aturannya ada di grammar input.

Cache

Cache aktif di kebijakan tetapi mati di wiring: tanpa argumen cache, client memakai DisabledCheckCache dan setiap pengecekan ulang ditagih lagi. Cache persisten butuh dua hal yang hanya bisa disediakan aplikasi Anda, yaitu driver SQLite dan penyimpan kunci platform:

val checkCache = SqlDelightCheckCache(
  driver = AndroidSqliteDriver(
    schema = SqlDelightCheckCache.Schema,
    context = context,
    name = "tracive-check-cache.db",
  ),
  dataKeyStore = KeystoreCacheDataKeyStore(context),   // milik Anda: SDK tidak menyertakannya
  ioContext = Dispatchers.IO,
)
  • · SDK tidak menyertakan implementasi CacheDataKeyStore untuk platform mana pun. Kuncinya harus dibuat dari sumber acak yang aman dan disimpan di Keystore atau Keychain; contoh acuannya ada di aplikasi sampel SDK.
  • · Hit tidak menyentuh jaringan, jalan saat offline, dan tidak memakan kredit. TTL bawaan mengikuti tier: HIGH 30 hari, ELEVATED 7 hari, CAUTION 24 jam, NONE 1 jam, dan cacheTtlSeconds dari server selalu menang.
  • · Identifier-nya sendiri tidak pernah ditulis ke disk: kunci barisnya SHA-256 dari searchQueryIndex, isinya AES-256-GCM.

Store yang sama juga mencatat hasil yang ditunjukkan pada satu transaksi, supaya layar detail transaksi tetap bisa menampilkannya berminggu-minggu kemudian tanpa pengecekan kedua. Setiap catatan adalah snapshot dengan retensi 90 hari. Jangan kirim hasil ini ke server Anda: ketentuan partner melarangnya, termasuk nilai turunan seperti tier atau jumlah laporan.

// Setelah transfer tercipta dan punya nomor referensi.
Tracive.client.cache.putTransactionCheck(transfer.reference, result)

// Di detail transaksi, kapan pun kemudian. null = tidak ada pengecekan; jangan tampilkan apa-apa.
Tracive.client.cache.getTransactionCheck(transfer.reference)?.let { renderSecurityCheckCard(it) }

Error & biaya

Semua error adalah subclass TraciveException. ExpiredToken tidak perlu Anda tangani: SDK me-refresh lalu mengulang sekali.

try {
  Tracive.client.checkScammer(query, idempotencyKey = attemptId)   // satu kunci per pengecekan
} catch (e: TraciveException.InsufficientCredits) {
  promptTopUp()                     // 402: jangan cek credits() sebelum setiap pengecekan
} catch (e: TraciveException.GrantRevoked) {
  reauthorize()                     // token sudah dihapus; jangan diulang
} catch (e: TraciveException.InvalidToken) {
  reauthorize()                     // belum ada grant yang bisa dipakai; tidak ada yang terkirim
} catch (e: TraciveException.RateLimited) {
  backOff(e.retryAfter)             // tidak pernah diulang otomatis
}
  • · Setiap request yang sampai ke server ditagih, termasuk miss.
  • · SDK hanya mengulang pengecekan yang gagal kalau Anda memberi idempotencyKey: 5xx atau timeout tidak memberi tahu apakah pengecekan pertama sudah ditagih. Satu kunci per pengecekan logis, dipakai ulang hanya untuk mengulang pengecekan yang sama.
  • · Jangan panggil credits() sebelum setiap pengecekan. Tangani 402-nya; pakai credits() untuk menampilkan saldo saja.

Keluar

Tracive.client.signOut()   // revoke best-effort, lalu token dan cache dihapus apa pun hasilnya