Skip to content

[Auth Refactor] Create New User Fix #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import android.util.Log
import com.cornellappdev.resell.android.BuildConfig
import com.cornellappdev.resell.android.model.core.UserInfoRepository
import com.cornellappdev.resell.android.model.login.FirebaseAuthRepository
import com.cornellappdev.resell.android.model.login.GoogleAuthRepository
import com.cornellappdev.resell.android.ui.screens.root.ResellRootRoute
import com.cornellappdev.resell.android.viewmodel.navigation.RootNavigationRepository
import com.cornellappdev.resell.android.viewmodel.root.RootConfirmationRepository
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONObject
import retrofit2.Retrofit
Expand All @@ -20,6 +26,9 @@ import javax.inject.Singleton
class RetrofitInstance @Inject constructor(
private val firebaseAuthRepository: FirebaseAuthRepository,
private val userInfoRepository: UserInfoRepository,
private val googleAuthRepository: GoogleAuthRepository,
private val rootNavigationRepository: RootNavigationRepository,
private val rootConfirmationRepository: RootConfirmationRepository,
) {
private var cachedToken: String? = null

Expand Down Expand Up @@ -65,7 +74,7 @@ class RetrofitInstance @Inject constructor(

// Get the `errors` response
try {
val jsonObject = JSONObject(responseBody)
val jsonObject = JSONObject(responseBody ?: "")
val errors = jsonObject.optJSONArray("errors")
if (errors != null) {
Log.e("OkHttpErrorResponse", "Errors: $errors")
Expand All @@ -76,13 +85,25 @@ class RetrofitInstance @Inject constructor(
}

response.newBuilder()
.body(ResponseBody.create(response.body?.contentType(), responseBody ?: ""))
.body((responseBody ?: "").toResponseBody(response.body?.contentType()))
.build()
}

private val authenticator = Authenticator { _, response ->
// Ping firebase for a refresh.
val accessToken = runBlocking { firebaseAuthRepository.getFirebaseAccessToken() }
if (responseCount(response) >= 2) {
// Already retried once, still getting 401 — force sign out
runBlocking {
googleAuthRepository.signOut()
rootNavigationRepository.navigate(ResellRootRoute.LANDING)
rootConfirmationRepository.showError(
message = "Authentication Failed. Please try signing in again!"
)
}
return@Authenticator null // Give up — don't retry again
}

// Ping firebase for a refresh. Force refresh.
val accessToken = runBlocking { firebaseAuthRepository.getFirebaseAccessToken(true) }
cachedToken = accessToken
if (accessToken != null) {
response.request.newBuilder()
Expand All @@ -94,6 +115,19 @@ class RetrofitInstance @Inject constructor(
}
}

/**
* Helper to count how many times we've already retried this request.
*/
private fun responseCount(response: Response): Int {
var count = 1
var priorResponse = response.priorResponse
while (priorResponse != null) {
count++
priorResponse = priorResponse.priorResponse
}
return count
}

// Build OkHttpClient with the dynamic auth interceptor
private val okHttpClient = OkHttpClient.Builder().apply {
if (BuildConfig.DEBUG) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface SettingsApiService {
suspend fun sendFeedback(@Body feedback: Feedback)

@POST("user")
suspend fun editUser(@Body user: EditUser)
suspend fun editUser(@Body user: EditUser): UserResponse
}

data class EditUser(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface UserApiService {
suspend fun logoutUser(@Body body: LogoutBody)

@POST("user/create")
suspend fun createUser(@Body createUserBody: CreateUserBody): UserResponse
suspend fun createUser(@Body createUserBody: CreateUserBody): User
}

data class LogoutBody(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ class FirebaseAuthRepository @Inject constructor(
*
* If retrieved successfully, will also store the access token in [UserInfoRepository]
* for use in the retrofit interceptor.
*
* @param forceRefresh Whether to force a firebase refresh of the access token.
*/
suspend fun getFirebaseAccessToken(): String? {
val token = firebaseAuth.currentUser?.getIdToken(false)?.await()?.token
suspend fun getFirebaseAccessToken(forceRefresh: Boolean = false): String? {
val token = firebaseAuth.currentUser?.getIdToken(forceRefresh)?.await()?.token
if (token == null) {
Log.e("FirebaseAuthRepository", "Access token is null.")
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class ResellAuthRepository @Inject constructor(
private val firebaseMessagingRepository: FirebaseMessagingRepository,
private val googleAuthRepository: GoogleAuthRepository,
) {
suspend fun createUser(createUserBody: CreateUserBody) =
suspend fun createUser(createUserBody: CreateUserBody): User =
retrofitInstance.userApi.createUser(createUserBody)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.cornellappdev.resell.android.model.api.Feedback
import com.cornellappdev.resell.android.model.api.ReportPostBody
import com.cornellappdev.resell.android.model.api.ReportProfileBody
import com.cornellappdev.resell.android.model.api.RetrofitInstance
import com.cornellappdev.resell.android.model.api.UserResponse
import com.cornellappdev.resell.android.model.core.UserInfoRepository
import com.cornellappdev.resell.android.model.login.FireStoreRepository
import com.cornellappdev.resell.android.model.profile.ProfileRepository
Expand Down Expand Up @@ -64,8 +65,8 @@ class SettingsRepository @Inject constructor(
venmo: String,
bio: String,
image: ImageBitmap?
) {
retrofitInstance.settingsApi.editUser(
): UserResponse {
val response = retrofitInstance.settingsApi.editUser(
EditUser(
username = username,
venmoHandle = venmo,
Expand All @@ -75,5 +76,7 @@ class SettingsRepository @Inject constructor(
)

profileRepository.fetchInternalProfile(userInfoRepository.getUserId()!!)

return response
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.cornellappdev.resell.android.ui.components.chat.messages

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.cornellappdev.resell.android.ui.components.main.ProfilePictureView
import com.cornellappdev.resell.android.ui.theme.ResellPreview
import com.cornellappdev.resell.android.util.LocalInfiniteShimmer

@Composable
fun MessageCardLoading(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.height(64.dp)
.fillMaxWidth()
.background(Color.White)
.padding(horizontal = 24.dp, vertical = 11.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row {
ProfilePictureView(
imageUrl = "",
modifier = Modifier
.fillMaxHeight()
.aspectRatio(1f)
.size(32.dp)
)
Spacer(modifier = Modifier.width(12.dp))
}

Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.padding(end = 16.dp),
verticalArrangement = Arrangement.SpaceEvenly
) {
Row(
modifier = Modifier.height(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
LoadingBlob(modifier = Modifier
.height(height = 20.dp)
.weight(1f))
}
Spacer(modifier = Modifier.height(1.dp))
LoadingBlob(modifier = Modifier.size(width = 100.dp, height = 20.dp))
}
}
}

@Composable
private fun LoadingBlob(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(100.dp),
color = LocalInfiniteShimmer.current
) {}
}

@Preview
@Composable
private fun MessageCardLoadingPreview() = ResellPreview {
MessageCardLoading()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.cornellappdev.resell.android.ui.components.chat.messages

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.cornellappdev.resell.android.ui.theme.ResellPreview

@Composable
fun MessagesScrollLoading(
modifier: Modifier = Modifier,
count: Int,
) {
LazyColumn(
contentPadding = PaddingValues(
bottom = 100.dp,
),
modifier = modifier,
) {
items(count = count) {
MessageCardLoading()
}
}
}

@Preview
@Composable
private fun MessagesScrollLoadingPreview() = ResellPreview {
MessagesScrollLoading(count = 3)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ fun ResellMessagesScroll(
onChatPressed: (ChatHeaderData) -> Unit,
listState: LazyListState,
modifier: Modifier = Modifier,
paddedTop: Dp = 0.dp,
) {
LazyColumn(
state = listState,
contentPadding = PaddingValues(
bottom = 100.dp,
top = paddedTop,
),
modifier = modifier,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.cornellappdev.resell.android.model.classes.ResellApiState
import com.cornellappdev.resell.android.ui.components.chat.messages.MessageTag
import com.cornellappdev.resell.android.ui.components.chat.messages.MessagesScrollLoading
import com.cornellappdev.resell.android.ui.components.chat.messages.ResellMessagesScroll
import com.cornellappdev.resell.android.ui.theme.Padding
import com.cornellappdev.resell.android.ui.theme.Style
Expand Down Expand Up @@ -69,7 +70,10 @@ fun MessagesScreen(
) {
when (chatUiState.loadedState) {
ResellApiState.Loading -> {
// TODO Loading State
MessagesScrollLoading(
modifier = Modifier.fillMaxSize(),
count = chatUiState.numLoadingChats
)
}

ResellApiState.Error -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class HomeViewModel @Inject constructor(
viewModelScope.launch {
try {
val posts = resellPostRepository.getPostsByPage(1)

// if current filter changed since then, do nothing
if (stateValue().activeFilter != HomeFilter.RECENT) return@launch

applyMutation {
copy(
listings = posts.map { it.toListing() },
Expand Down Expand Up @@ -103,6 +107,10 @@ class HomeViewModel @Inject constructor(
val posts = resellPostRepository.getPostsByFilter(
filter.name
)

// if current filter changed since then, do nothing
if (stateValue().activeFilter != filter) return@launch

applyMutation {
copy(
listings = posts.map { it.toListing() },
Expand Down Expand Up @@ -140,6 +148,10 @@ class HomeViewModel @Inject constructor(
val newPage = resellPostRepository.getPostsByPage(stateValue().page).map {
it.toListing()
}

// if current filter changed since then, do nothing
if (stateValue().activeFilter != HomeFilter.RECENT) return@launch

applyMutation {
copy(
listings = stateValue().listings + newPage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ class MessagesViewModel @Inject constructor(
}
}

val numLoadingChats: Int
get() = when (chatType) {
ChatType.Purchases -> 4
ChatType.Offers -> 2
}

val loadedState: ResellApiState
get() = filteredChats.toResellApiState()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import retrofit2.HttpException
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -170,6 +171,16 @@ class LandingViewModel @Inject constructor(
userInfoRepository.storeUserFromUserObject(user)
rootNavigationRepository.navigate(ResellRootRoute.MAIN)
}
} catch (e: HttpException) {
if (e.code() == 403) {
// 403 indicates that we need to create a new user.
rootNavigationRepository.navigate(ResellRootRoute.ONBOARDING)
}
else {
Log.e("LandingViewModel", "Error getting user: ", e)
onSignInFailed(showSheet = true)
rootConfirmationRepository.showError()
}
} catch (e: Exception) {
Log.e("LandingViewModel", "Error getting user: ", e)
onSignInFailed(showSheet = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class VenmoFieldViewModel @Inject constructor(
viewModelScope.launch {
val googleUser = googleAuthRepository.accountOrNull()!!
try {
val response = resellAuthRepository.createUser(
val user = resellAuthRepository.createUser(
CreateUserBody(
username = stateValue().username,
netid = googleUser.email!!.split("@")[0],
Expand All @@ -125,7 +125,7 @@ class VenmoFieldViewModel @Inject constructor(
)
)

userInfoRepository.storeUserFromUserObject(response.user)
userInfoRepository.storeUserFromUserObject(user)
rootNavigationRepository.navigate(ResellRootRoute.MAIN)
rootNavigationSheetRepository.showBottomSheet(RootSheet.Welcome)
} catch (e: Exception) {
Expand Down
Loading