Skip to content

TODO (IA — follow-up)

This document's information architecture overlaps with Android Integration. After an editorial decision, we'll consolidate one of the two as canonical. For now, use this document as an Account Merge / canonical_sub addendum. For the OAuth/storage base patterns, follow Android Integration.

Android (Kotlin) Integration

This document covers the standard pattern for integrating logi as an RP in an Android native app (regardless of Jetpack Compose / View XML). It's the Android mirror of the iOS Swift guide (Swift) and offers the same promises for the OAuth flow and credential storage.

For overall architecture recommendations, the Android Integration page has more detail; this document focuses on canonical_sub / linked_subs handling and the pattern for storing credentials separately.

Storing credentials

logi issues two client-side secrets:

SecretPurposeExpiry
refresh_tokenRefresh the access_token30 days (rotation)
device_secretThe primary PoP for an anonymous user, the device credentialIndefinite (device lifecycle)

For the storage pattern, follow the DataStore + Tink pattern in Android Integration — Storing the refresh token, but split the two secrets into separate DataStore keys (aliases) — so that revoking/wiping one doesn't affect the other.

No EncryptedSharedPreferences

EncryptedSharedPreferences in androidx.security:security-crypto is deprecated as of 1.1.0. In new code, use only the DataStore + Tink pattern. Never store device_secret in plain SharedPreferences either — it would be exposed in plaintext in backups.

Claims model

kotlin
@Serializable
data class LogiClaims(
    val sub: String,
    @SerialName("canonical_sub") val canonicalSub: String? = null,
    @SerialName("is_canonical") val isCanonical: Boolean? = null,
    @SerialName("linked_subs") val linkedSubs: List<String>? = null,
    @SerialName("previously_anonymous") val previouslyAnonymous: Boolean? = null,
    val email: String? = null,
    @SerialName("email_verified") val emailVerified: Boolean? = null,
    val anonymous: Boolean? = null
) {
    /** Always use this value for foreign-key / local-cache lookups. */
    val effectiveSub: String
        get() = canonicalSub ?: sub
}

Mapping the user after token verification

kotlin
class LogiSessionHandler(private val localStore: LocalStore) {
    fun handle(claims: LogiClaims) {
        val canonical = claims.effectiveSub
        val currentLocal = localStore.userId
        if (currentLocal != null && currentLocal != canonical) {
            // A merge happened — update the local cache to canonical.
            localStore.migrateUser(from = currentLocal, to = canonical)
        }
        localStore.userId = canonical

        if (claims.previouslyAnonymous == true) {
            localStore.previouslyAnonymous = true
        }
    }
}

Handling linked_subs

If the canonical account (isCanonical == true) absorbed old subs, pull the local-cache data into the canonical sub view:

kotlin
if (claims.isCanonical == true) {
    claims.linkedSubs?.forEach { absorbed ->
        localStore.reassignData(from = absorbed, to = claims.sub)
    }
}

Write reassignData to be idempotent — skip subs that have already been moved.

Detecting the Anonymous → Identified transition

kotlin
if (claims.anonymous == false && localStore.wasAnonymous) {
    // promotion happened: sync the device-local data from the anonymous era
    // to the promoted user's cloud account.
    syncEngine.runPromotionUpload()
    localStore.wasAnonymous = false
}

OAuth flow

The Authorization Code + PKCE flow uses the standard pattern on the Android page as-is. What this document covers is the canonical-aware layer that sits on top — for the OAuth redirect/state/code_verifier handling itself, see the existing guide.

Verifying the merge behavior

Before enabling this in production, confirm the following in staging:

  • [ ] When two SSOs happen on the same device (T1), the next token's canonical_sub is populated appropriately.
  • [ ] When signing up for the first time with an SSO of the same email (T2), linked_subs arrives immediately.
  • [ ] After running T3 (/me/merge via WebView), canonical changes on the next token rotation.
  • [ ] After an anonymous-first signup + SSO promotion, previously_anonymous == true.
  • [ ] The absorbed side's refresh token is invalid immediately (a refresh_token_revoked response).

Identity가 제품의 신뢰를 만듭니다.