Fix Quest Platform SDK auth flow

- Use asyncInitialize with message pump for proper SDK callback delivery
- Fall back to oculusId when numeric user ID unavailable (DUC pending)
- Gracefully handle missing nonce
- Auto-upgrade to full nonce validation when numeric ID becomes available
This commit is contained in:
2026-02-24 12:35:58 +01:00
parent 01a342582c
commit 609802dd92

View File

@@ -14,7 +14,6 @@ import com.meta.horizon.platform.ovr.models.UserProof
import com.meta.horizon.platform.ovr.requests.Request
import com.meta.horizon.platform.ovr.requests.Users
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -43,35 +42,43 @@ class LoginViewModel @Inject constructor(
_isLoading.value = true
_error.value = null
try {
// Initialize Platform SDK
// Initialize Platform SDK (async, wait for completion via message pump)
if (!Core.isInitialized()) {
Log.d(TAG, "Initializing Platform SDK with appId=$QUEST_APP_ID")
val initResult = Core.initialize(QUEST_APP_ID, activity.applicationContext)
Log.d(TAG, "Core.initialize returned: $initResult")
Log.d(TAG, "Async initializing Platform SDK with appId=$QUEST_APP_ID")
val initResult = awaitWithPump { Core.asyncInitialize(QUEST_APP_ID, activity.applicationContext) }
Log.d(TAG, "Platform SDK initialized: $initResult")
} else {
Log.d(TAG, "Platform SDK already initialized")
}
Log.d(TAG, "Core.isInitialized=${Core.isInitialized()}")
// Check cached user ID first (synchronous)
val cachedUserId = Core.getLoggedInUserID()
Log.d(TAG, "Core.getLoggedInUserID() = $cachedUserId")
// Get logged-in user (async with message pump)
Log.d(TAG, "Requesting logged-in user...")
val user = awaitWithPump { Users.getLoggedInUser() }
val numericId = user.getID().toString()
val oculusId = user.oculusID
Log.d(TAG, "User: id=$numericId displayName=${user.displayName} oculusId=$oculusId")
if (cachedUserId == 0L) {
throw Exception("Platform SDK returned user ID 0. " +
"Make sure the app is installed from the Horizon store (not sideloaded) " +
// Use numeric ID if available (requires Data Use Checkup), fall back to oculusId
val userId = if (user.getID() != 0L) numericId else oculusId
if (userId.isNullOrEmpty()) {
throw Exception("Platform SDK returned no user identifier. " +
"Make sure the app is installed from the Horizon store " +
"and your account is a test user for this app.")
}
// Get full user via async call with manual message pump
val user = awaitWithPump { Users.getLoggedInUser() }
val userId = user.getID().toString()
Log.d(TAG, "User: id=$userId displayName=${user.displayName}")
// Get user proof (nonce)
val proof = awaitWithPump { Users.getUserProof() }
val nonce = proof.value
Log.d(TAG, "UserProof nonce=$nonce")
// Get user proof (nonce) — may fail without DUC approval
var nonce = ""
try {
Log.d(TAG, "Requesting user proof...")
val proof = awaitWithPump { Users.getUserProof() }
nonce = proof.value
Log.d(TAG, "UserProof nonce length=${nonce.length}")
} catch (e: Exception) {
Log.w(TAG, "getUserProof failed (DUC pending?), proceeding without nonce", e)
}
// Send to backend for verification
Log.d(TAG, "Sending to backend: userId=$userId hasNonce=${nonce.isNotEmpty()}")
val response = apiService.metaCallback(
MetaCallbackRequest(
userId = userId,
@@ -82,6 +89,7 @@ class LoginViewModel @Inject constructor(
// Save session tokens
tokenStore.saveSession(response.accessToken, response.refreshToken)
Log.d(TAG, "Login successful!")
} catch (e: Exception) {
Log.e(TAG, "Quest login failed", e)
_error.value = e.message ?: "Login failed"
@@ -97,7 +105,7 @@ class LoginViewModel @Inject constructor(
/**
* Awaits a Platform SDK request by manually pumping the message queue.
* The SDK requires Request.runCallbacks() to be called for callbacks to fire.
* The SDK delivers responses via Core.popSDKMessage() which must be polled.
*/
private suspend fun <T> awaitWithPump(
block: () -> Request<T>,
@@ -105,25 +113,30 @@ class LoginViewModel @Inject constructor(
var completed = false
block()
.onSuccess { result: T ->
Log.d(TAG, "SDK request succeeded: ${result?.javaClass?.simpleName}")
if (!completed) {
completed = true
cont.resume(result)
}
}
.onError { error ->
Log.e(TAG, "SDK request failed: code=${error.code} http=${error.httpCode} msg=${error.message}")
if (!completed) {
completed = true
cont.resumeWithException(Exception(error.message))
cont.resumeWithException(Exception("SDK error ${error.code}: ${error.message}"))
}
}
// Pump messages on a background thread
Thread {
val timeout = System.currentTimeMillis() + 10_000 // 10s timeout
val timeout = System.currentTimeMillis() + 15_000 // 15s timeout
var msgCount = 0
while (!completed && System.currentTimeMillis() < timeout) {
try {
val msg = Core.popSDKMessage()
if (msg != null) {
msgCount++
Log.d(TAG, "Pumped message #$msgCount type=${msg.type} isError=${msg.isError}")
Request.handleMessage(msg)
}
} catch (e: Exception) {
@@ -131,9 +144,10 @@ class LoginViewModel @Inject constructor(
}
Thread.sleep(50)
}
Log.d(TAG, "Message pump done: completed=$completed msgs=$msgCount")
if (!completed) {
completed = true
cont.resumeWithException(Exception("Platform SDK request timed out"))
cont.resumeWithException(Exception("Platform SDK request timed out after 15s (pumped $msgCount messages)"))
}
}.start()
}