SDK Kotlin
Lihat di GitHubKotlin 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
// 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
)
}
}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. - ·
nulldarihandleCallbackberarti URL itu bukan redirect Tracive, jadi teruskan ke penanganan deep link Anda yang lain.
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 tampilSemua 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
CacheDataKeyStoreuntuk 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:
HIGH30 hari,ELEVATED7 hari,CAUTION24 jam,NONE1 jam, dancacheTtlSecondsdari 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; pakaicredits()untuk menampilkan saldo saja.
Keluar
Tracive.client.signOut() // revoke best-effort, lalu token dan cache dihapus apa pun hasilnya