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