Repository: Automattic/Gravatar-SDK-Android
Pull request #598: Use authenticated profile endpoint to fetch profile in the QE
Author: login: AdamGrzybkowski
Labels: name: enhancement
description: Enhancement
name: [Feature] Gravatar-Quickeditor
description: Gravatar Quick Editor module
Created: 2025-03-21T12:04:55Z | Merged: 2025-03-28T09:07:59Z

Closes #596 

### Description

A new endpoint to get a profile that takes the auth token was added. This improves the flow when using the QE in the Jetpack app for a user who hasn't opened the Quick Editor before because it will create an identity for him. 

Instead of seeing an empty profile card on the initial load, the user will see his profile now.

### Testing Steps

Techincally, this can be tested with the demo app with the following steps:

1. Create a new account on WordPress.com and verify email
2. Obtaing the auth token 
3. Make sure the QE is not opened before that anywhere
4. Run the demo app
5. Enable the bearer token and paste it
<img width="590" alt="Screenshot 2025-03-24 at 10 10 21" src="https://github.com/user-attachments/assets/0343f03f-4a8a-4d50-ac9f-c67daccd3a87" />

6. Tap on the "Update Avatar"
7. Conirm the profile card shows the profile (the username should be visible)


Linked issue #596: Use an authorized profile endpoint for the QE

Diff:
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorContainer.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorContainer.kt
index aabab40a3..9f1c36043 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorContainer.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorContainer.kt
@@ -13,6 +13,7 @@ import com.gravatar.quickeditor.data.FileUtils
 import com.gravatar.quickeditor.data.ImageDownloader
 import com.gravatar.quickeditor.data.datastore.createEncryptedFileWithFallbackReset
 import com.gravatar.quickeditor.data.repository.AvatarRepository
+import com.gravatar.quickeditor.data.repository.ProfileRepository
 import com.gravatar.quickeditor.data.storage.AvatarStorage
 import com.gravatar.quickeditor.data.storage.DataStoreProfileStorage
 import com.gravatar.quickeditor.data.storage.DataStoreTokenStorage
@@ -88,6 +89,14 @@ internal class QuickEditorContainer private constructor(
         ProfileService()
     }
 
+    val profileRepository: ProfileRepository by lazy {
+        ProfileRepository(
+            profileService = profileService,
+            tokenStorage = tokenStorage,
+            dispatcher = Dispatchers.IO,
+        )
+    }
+
     val fileUtils: FileUtils by lazy {
         FileUtils(context)
     }
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/repository/ProfileRepository.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/repository/ProfileRepository.kt
new file mode 100644
index 000000000..5fc36c11a
--- /dev/null
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/repository/ProfileRepository.kt
@@ -0,0 +1,26 @@
+package com.gravatar.quickeditor.data.repository
+
+import com.gravatar.quickeditor.data.models.QuickEditorError
+import com.gravatar.quickeditor.data.storage.TokenStorage
+import com.gravatar.restapi.models.Profile
+import com.gravatar.services.GravatarResult
+import com.gravatar.services.ProfileService
+import com.gravatar.types.Email
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+internal class ProfileRepository(
+    private val tokenStorage: TokenStorage,
+    private val profileService: ProfileService,
+    private val dispatcher: CoroutineDispatcher,
+) {
+    suspend fun getProfile(email: Email): GravatarResult<Profile, QuickEditorError> = withContext(dispatcher) {
+        val token = tokenStorage.getToken(email.hash().toString())
+        token?.let {
+            when (val result = profileService.retrieveAuthenticatedCatching(token)) {
+                is GravatarResult.Success -> GravatarResult.Success(result.value)
+                is GravatarResult.Failure -> GravatarResult.Failure(QuickEditorError.Request(result.error))
+            }
+        } ?: GravatarResult.Failure(QuickEditorError.TokenNotFound)
+    }
+}
diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt
index a91b81a33..97fc50bda 100644
--- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt
+++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt
@@ -11,6 +11,7 @@ import com.gravatar.quickeditor.data.FileUtils
 import com.gravatar.quickeditor.data.ImageDownloader
 import com.gravatar.quickeditor.data.models.QuickEditorError
 import com.gravatar.quickeditor.data.repository.AvatarRepository
+import com.gravatar.quickeditor.data.repository.ProfileRepository
 import com.gravatar.quickeditor.ui.editor.AvatarPickerContentLayout
 import com.gravatar.quickeditor.ui.editor.GravatarQuickEditorParams
 import com.gravatar.quickeditor.ui.time.Clock
@@ -18,7 +19,6 @@ import com.gravatar.quickeditor.ui.time.SystemClock
 import com.gravatar.restapi.models.Avatar
 import com.gravatar.services.ErrorType
 import com.gravatar.services.GravatarResult
-import com.gravatar.services.ProfileService
 import com.gravatar.types.Email
 import com.gravatar.ui.components.ComponentState
 import kotlinx.coroutines.channels.Channel
@@ -39,7 +39,7 @@ internal class AvatarPickerViewModel(
     private val email: Email,
     private val handleExpiredSession: Boolean,
     private val avatarPickerContentLayout: AvatarPickerContentLayout,
-    private val profileService: ProfileService,
+    private val profileRepository: ProfileRepository,
     private val avatarRepository: AvatarRepository,
     private val imageDownloader: ImageDownloader,
     private val fileUtils: FileUtils,
@@ -276,7 +276,7 @@ internal class AvatarPickerViewModel(
     private fun fetchProfile() {
         viewModelScope.launch {
             _uiState.update { currentState -> currentState.copy(profile = ComponentState.Loading) }
-            when (val result = profileService.retrieveCatching(email)) {
+            when (val result = profileRepository.getProfile(email)) {
                 is GravatarResult.Success -> {
                     _uiState.update { currentState ->
                         currentState.copy(profile = ComponentState.Loaded(result.value))
@@ -474,7 +474,7 @@ internal class AvatarPickerViewModelFactory(
             handleExpiredSession = handleExpiredSession,
             email = gravatarQuickEditorParams.email,
             avatarPickerContentLayout = gravatarQuickEditorParams.avatarPickerContentLayout,
-            profileService = QuickEditorContainer.getInstance().profileService,
+            profileRepository = QuickEditorContainer.getInstance().profileRepository,
             avatarRepository = QuickEditorContainer.getInstance().avatarRepository,
             imageDownloader = QuickEditorContainer.getInstance().imageDownloader,
             fileUtils = QuickEditorContainer.getInstance().fileUtils,
diff --git a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/data/repository/ProfileRepositoryTest.kt b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/data/repository/ProfileRepositoryTest.kt
new file mode 100644
index 000000000..47c176396
--- /dev/null
+++ b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/data/repository/ProfileRepositoryTest.kt
@@ -0,0 +1,86 @@
+package com.gravatar.quickeditor.data.repository
+
+import com.gravatar.extensions.defaultProfile
+import com.gravatar.quickeditor.data.models.QuickEditorError
+import com.gravatar.quickeditor.data.storage.TokenStorage
+import com.gravatar.quickeditor.ui.CoroutineTestRule
+import com.gravatar.quickeditor.ui.avatarpicker.EmailAvatars
+import com.gravatar.restapi.models.Profile
+import com.gravatar.services.ErrorType
+import com.gravatar.services.GravatarResult
+import com.gravatar.services.ProfileService
+import com.gravatar.types.Email
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class ProfileRepositoryTest {
+    private val testDispatcher = StandardTestDispatcher()
+
+    @get:Rule
+    var coroutinesTestRule = CoroutineTestRule(testDispatcher)
+
+    private lateinit var profileRepository: ProfileRepository
+    private val profileService: ProfileService = mockk()
+    private val tokenStorage: TokenStorage = mockk()
+
+    private val email = Email("email")
+
+    @Before
+    fun setUp() {
+        profileRepository = ProfileRepository(
+            profileService = profileService,
+            tokenStorage = tokenStorage,
+            dispatcher = testDispatcher,
+        )
+    }
+
+    @Test
+    fun `given email when token not found then TokenNotFound result`() = runTest {
+        coEvery { tokenStorage.getToken(any()) } returns null
+
+        val result = profileRepository.getProfile(email)
+
+        assertEquals(
+            GravatarResult.Failure<EmailAvatars, QuickEditorError>(QuickEditorError.TokenNotFound),
+            result,
+        )
+    }
+
+    @Test
+    fun `given oauthToken when retrieving profile and data is returned then result is successful`() = runTest {
+        val oauthToken = "oauth"
+        val profileResult = defaultProfile(hash = "hash")
+        coEvery { tokenStorage.getToken(any()) } returns oauthToken
+        coEvery {
+            profileService.retrieveAuthenticatedCatching(oauthToken)
+        } returns GravatarResult.Success(profileResult)
+
+        val profile = profileRepository.getProfile(email)
+
+        coVerify(exactly = 1) { profileService.retrieveAuthenticatedCatching(oauthToken) }
+        assertEquals(GravatarResult.Success<Profile, QuickEditorError>(profileResult), profile)
+    }
+
+    @Test
+    fun `given oauthToken when retrieving profile and response is NOT successful then Failure returned`() = runTest {
+        val oauthToken = "oauth"
+        coEvery { tokenStorage.getToken(any()) } returns oauthToken
+        coEvery {
+            profileService.retrieveAuthenticatedCatching(oauthToken)
+        } returns GravatarResult.Failure(ErrorType.Server)
+
+        val result = profileRepository.getProfile(email)
+
+        assertEquals(
+            GravatarResult.Failure<Profile, QuickEditorError>(QuickEditorError.Request(ErrorType.Server)),
+            result,
+        )
+    }
+}
diff --git a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt
index 7ee4b9cbe..e9558e838 100644
--- a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt
+++ b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt
@@ -9,6 +9,7 @@ import com.gravatar.quickeditor.data.FileUtils
 import com.gravatar.quickeditor.data.ImageDownloader
 import com.gravatar.quickeditor.data.models.QuickEditorError
 import com.gravatar.quickeditor.data.repository.AvatarRepository
+import com.gravatar.quickeditor.data.repository.ProfileRepository
 import com.gravatar.quickeditor.ui.CoroutineTestRule
 import com.gravatar.quickeditor.ui.editor.AvatarPickerContentLayout
 import com.gravatar.quickeditor.ui.time.Clock
@@ -16,7 +17,6 @@ import com.gravatar.restapi.models.Avatar
 import com.gravatar.restapi.models.Error
 import com.gravatar.services.ErrorType
 import com.gravatar.services.GravatarResult
-import com.gravatar.services.ProfileService
 import com.gravatar.types.Email
 import com.gravatar.ui.components.ComponentState
 import io.mockk.coEvery
@@ -42,7 +42,7 @@ class AvatarPickerViewModelTest {
     @get:Rule
     var coroutineTestRule = CoroutineTestRule(testDispatcher)
 
-    private val profileService = mockk<ProfileService>()
+    private val profileRepository = mockk<ProfileRepository>()
     private val avatarRepository = mockk<AvatarRepository>()
     private val fileUtils = mockk<FileUtils>()
     private val imageDownloader = mockk<ImageDownloader>()
@@ -68,7 +68,9 @@ class AvatarPickerViewModelTest {
 
     @Before
     fun setup() {
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Failure(ErrorType.Unknown())
+        coEvery {
+            profileRepository.getProfile(email)
+        } returns GravatarResult.Failure(QuickEditorError.Request(ErrorType.Server))
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(emptyList())
         coEvery { avatarRepository.getAvatars(email) } returns avatarsFlow
         coEvery { clock.getTimeMillis() } returns 0
@@ -133,7 +135,7 @@ class AvatarPickerViewModelTest {
 
     @Test
     fun `given view model initialization when fetch profile successful then uiState is updated`() = runTest {
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
 
         viewModel = initViewModel()
 
@@ -208,7 +210,7 @@ class AvatarPickerViewModelTest {
     fun `given avatar when selected successful then uiState is updated`() = runTest {
         val avatars = avatars.selectAvatarId("1")
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.selectAvatar(any(), any()) } returns GravatarResult.Success(Unit)
 
         viewModel = initViewModel()
@@ -251,7 +253,7 @@ class AvatarPickerViewModelTest {
     fun `given avatar when selected failure then uiState is updated`() = runTest {
         val avatars = avatars.selectAvatarId("1")
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.selectAvatar(any(), any()) } returns GravatarResult.Failure(QuickEditorError.Unknown)
 
         viewModel = initViewModel()
@@ -290,7 +292,7 @@ class AvatarPickerViewModelTest {
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun `given avatar when reselected then nothing happens`() = runTest {
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.selectAvatar(any(), any()) } returns GravatarResult.Success(Unit)
 
         viewModel = initViewModel()
@@ -327,7 +329,7 @@ class AvatarPickerViewModelTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("1")
         every { fileUtils.deleteFile(any()) } returns Unit
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         val uploadedAvatar = createAvatar("3")
         coEvery { avatarRepository.uploadAvatar(any(), any()) } returns GravatarResult.Success(uploadedAvatar)
         coEvery { avatarRepository.refreshAvatars(any()) } returns GravatarResult.Success(avatars)
@@ -372,7 +374,7 @@ class AvatarPickerViewModelTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("1")
         every { fileUtils.deleteFile(any()) } returns Unit
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.uploadAvatar(any(), any()) } returns GravatarResult.Failure(invalidRequest)
 
         coEvery { avatarRepository.refreshAvatars(any()) } returns GravatarResult.Success(avatars)
@@ -418,7 +420,7 @@ class AvatarPickerViewModelTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("2")
         every { fileUtils.deleteFile(any()) } returns Unit
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery {
             avatarRepository.uploadAvatar(any(), any())
         } returns GravatarResult.Success(createAvatar("3"))
@@ -452,7 +454,7 @@ class AvatarPickerViewModelTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("2")
         every { fileUtils.deleteFile(any()) } returns Unit
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery {
             avatarRepository.uploadAvatar(any(), any())
         } returns GravatarResult.Failure(QuickEditorError.Request(ErrorType.Server))
@@ -487,7 +489,7 @@ class AvatarPickerViewModelTest {
         val uriTwo = mockk<Uri>()
         val avatars = listOf(createAvatar("3", isSelected = true))
         every { fileUtils.deleteFile(any()) } returns Unit
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.refreshAvatars(any()) } returns GravatarResult.Success(avatars)
         coEvery { avatarRepository.uploadAvatar(any(), any()) } returns GravatarResult.Failure(invalidRequest)
 
@@ -549,7 +551,7 @@ class AvatarPickerViewModelTest {
     fun `given failed avatar upload when FailedAvatarTapped then UiState updated`() = runTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("1")
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.uploadAvatar(any(), any()) } returns GravatarResult.Failure(invalidRequest)
         coEvery { avatarRepository.refreshAvatars(any()) } returns GravatarResult.Success(avatars)
 
@@ -571,7 +573,7 @@ class AvatarPickerViewModelTest {
     fun `given failed avatar upload dialog shown when FailedAvatarDismissed then UiState updated`() = runTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("1")
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery {
             avatarRepository.uploadAvatar(any(), any())
         } returns GravatarResult.Failure(QuickEditorError.Request(ErrorType.Server))
@@ -598,7 +600,7 @@ class AvatarPickerViewModelTest {
     fun `given failed avatar upload dialog shown when FailedAvatarDialogDismissed then UiState updated`() = runTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("1")
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.uploadAvatar(any(), any()) } returns GravatarResult.Failure(invalidRequest)
         coEvery { avatarRepository.refreshAvatars(any()) } returns GravatarResult.Success(avatars)
 
@@ -623,7 +625,7 @@ class AvatarPickerViewModelTest {
     fun `given failed avatar upload dialog shown when ImageCropped then UiState updated`() = runTest {
         val uri = mockk<Uri>()
         val avatars = avatars.selectAvatarId("1")
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery {
             avatarRepository.uploadAvatar(any(), any())
         } returns GravatarResult.Failure(QuickEditorError.Request(ErrorType.Server))
@@ -650,7 +652,7 @@ class AvatarPickerViewModelTest {
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun `given profile loaded when refresh then fetch profile not called`() = runTest {
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Failure(QuickEditorError.Unknown)
         viewModel = initViewModel()
         advanceUntilIdle()
@@ -658,7 +660,7 @@ class AvatarPickerViewModelTest {
         viewModel.onEvent(AvatarPickerEvent.Refresh)
         advanceUntilIdle()
 
-        coVerify(exactly = 1) { profileService.retrieveCatching(email) }
+        coVerify(exactly = 1) { profileRepository.getProfile(email) }
     }
 
     @Suppress("LongMethod")
@@ -668,7 +670,7 @@ class AvatarPickerViewModelTest {
         val uriOne = mockk<Uri>()
         val avatars = listOf(createAvatar("3", isSelected = false))
         every { fileUtils.deleteFile(any()) } returns Unit
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.refreshAvatars(any()) } returns GravatarResult.Success(avatars)
         val uploadedAvatar = createAvatar(id = "1", isSelected = true)
         coEvery { avatarRepository.uploadAvatar(any(), any()) } returns GravatarResult.Success(uploadedAvatar)
@@ -729,7 +731,7 @@ class AvatarPickerViewModelTest {
     fun `given selected avatar when delete successful then uiState is updated`() = runTest {
         val avatars = avatars.selectAvatarId(avatars.first().imageId)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.deleteAvatar(any(), any()) } returns GravatarResult.Success(Unit)
 
         viewModel = initViewModel()
@@ -781,7 +783,7 @@ class AvatarPickerViewModelTest {
     fun `given non selected avatar when delete successful then uiState is updated`() = runTest {
         val avatars = avatars.selectAvatarId(avatars.first().imageId)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.deleteAvatar(any(), any()) } returns GravatarResult.Success(Unit)
 
         viewModel = initViewModel()
@@ -817,7 +819,7 @@ class AvatarPickerViewModelTest {
     fun `given selected avatar when delete fails then uiState is updated`() = runTest {
         val avatars = avatars.selectAvatarId(avatars.first().imageId)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.deleteAvatar(any(), any()) } returns GravatarResult.Failure(QuickEditorError.Unknown)
 
         viewModel = initViewModel()
@@ -856,7 +858,7 @@ class AvatarPickerViewModelTest {
     fun `given non selected avatar when delete fails then uiState is updated`() = runTest {
         val avatars = avatars.selectAvatarId(avatars.first().imageId)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.deleteAvatar(any(), any()) } returns GravatarResult.Failure(QuickEditorError.Unknown)
 
         viewModel = initViewModel()
@@ -897,7 +899,7 @@ class AvatarPickerViewModelTest {
         runTest {
             val uri = mockk<Uri>()
             every { fileUtils.deleteFile(any()) } returns Unit
-            coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+            coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
             coEvery { avatarRepository.refreshAvatars(any()) } returns GravatarResult.Success(avatars)
             val uploadedAvatar = createAvatar("3", isSelected = true)
             coEvery { avatarRepository.uploadAvatar(any(), any()) } returns GravatarResult.Success(uploadedAvatar)
@@ -963,7 +965,7 @@ class AvatarPickerViewModelTest {
     fun `given avatar already deleted when delete returns 404 then uiState is updated`() = runTest {
         val avatars = avatars.selectAvatarId(avatars.first().imageId)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.deleteAvatar(any(), any()) } returns
             GravatarResult.Failure(QuickEditorError.Request(ErrorType.NotFound))
 
@@ -1006,7 +1008,7 @@ class AvatarPickerViewModelTest {
     fun `given avatar when download queued then AvatarDownloadStarted sent`() = runTest {
         val avatars = avatars.selectAvatarId("1")
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.selectAvatar(any(), any()) } returns GravatarResult.Success(Unit)
         coEvery { imageDownloader.downloadImage(any()) } returns GravatarResult.Success(Unit)
 
@@ -1026,7 +1028,7 @@ class AvatarPickerViewModelTest {
     fun `given avatar when download manager disabled then uiState updated`() = runTest {
         val emailAvatarsCopy = avatars.selectAvatarId("1")
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(emailAvatarsCopy)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.selectAvatar(any(), any()) } returns GravatarResult.Success(Unit)
         coEvery {
             imageDownloader.downloadImage(any())
@@ -1048,7 +1050,7 @@ class AvatarPickerViewModelTest {
     fun `given avatar when download manager not available then DownloadManagerNotAvailable sent`() = runTest {
         val avatars = avatars.selectAvatarId("1")
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery { avatarRepository.selectAvatar(any(), any()) } returns GravatarResult.Success(Unit)
         coEvery {
             imageDownloader.downloadImage(any())
@@ -1073,7 +1075,7 @@ class AvatarPickerViewModelTest {
         val oldAvatar = createAvatar(avatarId, isSelected = true)
         val avatars = listOf(oldAvatar)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         val updatedAvatar = oldAvatar.copy(rating)
         coEvery {
             avatarRepository.updateAvatar(email, avatarId, rating)
@@ -1111,7 +1113,7 @@ class AvatarPickerViewModelTest {
         val oldAvatar = createAvatar(avatarId, isSelected = true)
         val avatars = listOf(oldAvatar)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery {
             avatarRepository.updateAvatar(email, avatarId, rating)
         } returns GravatarResult.Failure(QuickEditorError.Unknown)
@@ -1161,7 +1163,7 @@ class AvatarPickerViewModelTest {
         val oldAvatar = createAvatar(avatarId, isSelected = true, rating = rating, altText = altText)
         val avatars = listOf(oldAvatar)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
         coEvery {
             avatarRepository.updateAvatar(email, avatarId, rating)
         } returns GravatarResult.Success(oldAvatar)
@@ -1183,7 +1185,7 @@ class AvatarPickerViewModelTest {
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun `given empty avatar list when loaded then avatar not selected banner invisible`() = runTest {
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
 
         viewModel = initViewModel()
 
@@ -1200,7 +1202,7 @@ class AvatarPickerViewModelTest {
         val avatar = createAvatar("1", isSelected = true)
         val avatars = listOf(avatar)
         coEvery { avatarRepository.refreshAvatars(email) } returns GravatarResult.Success(avatars)
-        coEvery { profileService.retrieveCatching(email) } returns GravatarResult.Success(profile)
+        coEvery { profileRepository.getProfile(email) } returns GravatarResult.Success(profile)
 
         viewModel = initViewModel()
 
@@ -1234,7 +1236,7 @@ class AvatarPickerViewModelTest {
         email = email,
         handleExpiredSession = handleExpiredSession,
         avatarPickerContentLayout = avatarPickerContentLayout,
-        profileService = profileService,
+        profileRepository = profileRepository,
         avatarRepository = avatarRepository,
         fileUtils = fileUtils,
         imageDownloader = imageDownloader,
diff --git a/gravatar/api/gravatar.api b/gravatar/api/gravatar.api
index 14dc572ca..aca1b9695 100644
--- a/gravatar/api/gravatar.api
+++ b/gravatar/api/gravatar.api
@@ -944,6 +944,8 @@ public final class com/gravatar/services/ProfileService {
 	public final fun retrieve (Lcom/gravatar/types/Email;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public final fun retrieve (Lcom/gravatar/types/Hash;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public final fun retrieve (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public final fun retrieveAuthenticated (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public final fun retrieveAuthenticatedCatching (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public final fun retrieveByUsername (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public final fun retrieveByUsernameCatching (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public final fun retrieveCatching (Lcom/gravatar/types/Email;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
diff --git a/gravatar/openapi/api-gravatar.json b/gravatar/openapi/api-gravatar.json
index e684c4ee9..dbf1a758b 100644
--- a/gravatar/openapi/api-gravatar.json
+++ b/gravatar/openapi/api-gravatar.json
@@ -774,6 +774,61 @@
         }
       }
     },
+    "/me/profile": {
+      "get": {
+        "summary": "Get profile information for the authenticated user",
+        "description": "Returns the information available for the authenticated user. It's equivalent to the full profile information available in the `/profiles/{profileIdentifier}` endpoint.",
+        "tags": [
+          "profiles"
+        ],
+        "operationId": "getProfile",
+        "security": [
+          {
+            "oauth": []
+          }
+        ],
+        "parameters": [],
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "$ref": "#/components/schemas/Profile"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Not Authorized",
+            "$ref": "#/components/responses/not_authorized"
+          },
+          "403": {
+            "description": "Insufficient Scope",
+            "$ref": "#/components/responses/insufficient_scope"
+          },
+          "404": {
+            "description": "Profile is disabled",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "disabled": {
+                    "value": {
+                      "error": "Profile is disabled",
+                      "code": "disabled"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/me/associated-email": {
       "get": {
         "summary": "Check if the email is associated with the authenticated user",
diff --git a/gravatar/src/main/java/com/gravatar/restapi/apis/ProfilesApi.kt b/gravatar/src/main/java/com/gravatar/restapi/apis/ProfilesApi.kt
index c9f605a18..29f96b840 100644
--- a/gravatar/src/main/java/com/gravatar/restapi/apis/ProfilesApi.kt
+++ b/gravatar/src/main/java/com/gravatar/restapi/apis/ProfilesApi.kt
@@ -31,6 +31,20 @@ internal interface ProfilesApi {
         @Query("email_hash") emailHash: kotlin.String,
     ): Response<AssociatedResponse>
 
+    /**
+     * Get profile information for the authenticated user
+     * Returns the information available for the authenticated user. It&#39;s equivalent to the full profile information available in the &#x60;/profiles/{profileIdentifier}&#x60; endpoint.
+     * Responses:
+     *  - 200: Successful response
+     *  - 401: Not Authorized
+     *  - 403: Insufficient Scope
+     *  - 404: Profile is disabled
+     *
+     * @return [Profile]
+     */
+    @GET("me/profile")
+    suspend fun getProfile(): Response<Profile>
+
     /**
      * Get profile by identifier
      * Returns a profile by the given identifier.
diff --git a/gravatar/src/main/java/com/gravatar/services/ProfileService.kt b/gravatar/src/main/java/com/gravatar/services/ProfileService.kt
index 3d4e6aed9..1d2c35984 100644
--- a/gravatar/src/main/java/com/gravatar/services/ProfileService.kt
+++ b/gravatar/src/main/java/com/gravatar/services/ProfileService.kt
@@ -178,4 +178,41 @@ public class ProfileService(private val okHttpClient: OkHttpClient? = null) {
     ): GravatarResult<Boolean, ErrorType> = runCatchingRequest {
         checkAssociatedEmail(oauthToken, email)
     }
+
+    /**
+     * Retrieves the profile information for the authenticated user.
+     *
+     * @param withToken The OAuth token to use for authentication
+     * @return The profile information for the authenticated user
+     */
+    public suspend fun retrieveAuthenticated(withToken: String): Profile = runThrowingExceptionRequest {
+        withContext(GravatarSdkDI.dispatcherIO) {
+            val service = GravatarSdkDI.getGravatarV3Service(okHttpClient, withToken)
+
+            val response = service.getProfile()
+            if (response.isSuccessful) {
+                response.body() ?: error("Response body is null")
+            } else {
+                // Log the response body for debugging purposes if the response is not successful
+                Logger.w(
+                    LOG_TAG,
+                    "Network call unsuccessful trying to get Gravatar profile: ${response.code()}",
+                )
+                throw HttpException(response)
+            }
+        }
+    }
+
+    /**
+     * Retrieves the profile information for the authenticated user.
+     * This method will catch any exception that occurs during
+     * the execution and return it as a [GravatarResult.Failure].
+     *
+     * @param withToken The OAuth token to use for authentication
+     * @return The profile information for the authenticated user
+     */
+    public suspend fun retrieveAuthenticatedCatching(withToken: String): GravatarResult<Profile, ErrorType> =
+        runCatchingRequest {
+            retrieveAuthenticated(withToken)
+        }
 }
diff --git a/gravatar/src/test/java/com/gravatar/services/ProfileServiceTests.kt b/gravatar/src/test/java/com/gravatar/services/ProfileServiceTests.kt
index a1154870e..124f45707 100644
--- a/gravatar/src/test/java/com/gravatar/services/ProfileServiceTests.kt
+++ b/gravatar/src/test/java/com/gravatar/services/ProfileServiceTests.kt
@@ -342,4 +342,60 @@ class ProfileServiceTests {
 
             profileService.checkAssociatedEmail(oauthToken, usernameEmail)
         }
+
+    @Test
+    fun `given oauthToken when retrieving profile and data is returned then Profile returned`() = runTest {
+        val oauthToken = "oauth"
+        val mockResponse = mockk<Response<Profile>> {
+            every { isSuccessful } returns true
+            every { body() } returns mockk()
+        }
+        coEvery { containerRule.gravatarApiMock.getProfile() } returns mockResponse
+
+        val profile = profileService.retrieveAuthenticated(oauthToken)
+
+        coVerify(exactly = 1) { containerRule.gravatarApiMock.getProfile() }
+        assertEquals(mockResponse.body(), profile)
+    }
+
+    @Test
+    fun `given oauthToken when retrieving profile and response is NOT successful then exception is thrown`() =
+        runTestExpectingGravatarException(ErrorType.Unauthorized, HttpException::class.java) {
+            val oauthToken = "oauth"
+            val mockResponse = mockk<Response<Profile>> {
+                every { isSuccessful } returns false
+                every { code() } returns 401
+                every { errorBody() } returns mockk(relaxed = true)
+            }
+            coEvery { containerRule.gravatarApiMock.getProfile() } returns mockResponse
+
+            profileService.retrieveAuthenticated(oauthToken)
+        }
+
+    @Test
+    fun `given oauthToken when retrieving profile and an exception is thrown then result is failure`() = runTest {
+        val oauthToken = "oauth"
+        coEvery { containerRule.gravatarApiMock.getProfile() } throws Exception()
+
+        val result = profileService.retrieveAuthenticatedCatching(oauthToken)
+
+        coVerify(exactly = 1) { containerRule.gravatarApiMock.getProfile() }
+        assertTrue((result as GravatarResult.Failure).error == ErrorType.Unknown())
+    }
+
+    @Test
+    fun `given oauthToken when retrieving profile and data is returned then result is successful`() = runTest {
+        val oauthToken = "oauth"
+        val mockResponse = mockk<Response<Profile>> {
+            every { isSuccessful } returns true
+            every { body() } returns mockk()
+        }
+        coEvery { containerRule.gravatarApiMock.getProfile() } returns mockResponse
+
+        val result = profileService.retrieveAuthenticatedCatching(oauthToken)
+
+        coVerify(exactly = 1) { containerRule.gravatarApiMock.getProfile() }
+        assertTrue(result is GravatarResult.Success)
+        assertEquals(mockResponse.body(), (result as GravatarResult.Success).value)
+    }
 }


Reviews:
user: login: AdamGrzybkowski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T09:15:54Z
user: login: etoledom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T03:20:03Z
user: login: etoledom
body: Looks good!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T03:28:15Z
user: login: AdamGrzybkowski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T08:49:26Z

Review comments:
diff_hunk: @@ -178,4 +178,41 @@ public class ProfileService(private val okHttpClient: OkHttpClient? = null) {
     ): GravatarResult<Boolean, ErrorType> = runCatchingRequest {
         checkAssociatedEmail(oauthToken, email)
     }
+
+    /**
+     * Retrieves the profile information for the authenticated user.
+     *
+     * @param oauthToken The OAuth token to use for authentication
+     * @return The profile information for the authenticated user
+     */
+    public suspend fun retrieveProfile(oauthToken: String): Profile = runThrowingExceptionRequest {
path: gravatar/src/main/java/com/gravatar/services/ProfileService.kt
user: login: AdamGrzybkowski
body: I'm not convinced about this name. We use `retrieve` in other places but the `retrieve(String)` is already used, so that won't work. I'm leaning towards sth like `retrieveWithToken` or `retrieveAuthenticated` to highlight the difference. Thoughts? 
created_at: 2025-03-24T09:15:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,4 +178,41 @@ public class ProfileService(private val okHttpClient: OkHttpClient? = null) {
     ): GravatarResult<Boolean, ErrorType> = runCatchingRequest {
         checkAssociatedEmail(oauthToken, email)
     }
+
+    /**
+     * Retrieves the profile information for the authenticated user.
+     *
+     * @param oauthToken The OAuth token to use for authentication
+     * @return The profile information for the authenticated user
+     */
+    public suspend fun retrieveProfile(oauthToken: String): Profile = runThrowingExceptionRequest {
path: gravatar/src/main/java/com/gravatar/services/ProfileService.kt
user: login: etoledom
body: I personally like to read `profile` in the name, something like `profileAuthenticated(withToken:)`, to know explicitly on name that this method will return a profile (and it's also easier to search all the get profile options via autocomplete).

For consistency, I think something like `retrieveAuthenticated(withToken:)` would be a good fit following the naming pattern already in use.
created_at: 2025-03-28T03:20:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -178,4 +178,41 @@ public class ProfileService(private val okHttpClient: OkHttpClient? = null) {
     ): GravatarResult<Boolean, ErrorType> = runCatchingRequest {
         checkAssociatedEmail(oauthToken, email)
     }
+
+    /**
+     * Retrieves the profile information for the authenticated user.
+     *
+     * @param oauthToken The OAuth token to use for authentication
+     * @return The profile information for the authenticated user
+     */
+    public suspend fun retrieveProfile(oauthToken: String): Profile = runThrowingExceptionRequest {
path: gravatar/src/main/java/com/gravatar/services/ProfileService.kt
user: login: AdamGrzybkowski
body: Thanks for your input! I went with `retrieveAuthenticated`. 
created_at: 2025-03-28T08:49:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: wpmobilebot
created_at: 2025-03-21T12:09:19Z
body: <!-- REUSE_ID: app-prototype-build-link -->

<details><summary>📲 You can test the changes from this Pull Request in <b>Gravatar Demo</b> by scanning the QR code below to install the corresponding build.</summary>
<table>
<tr>
  <td rowspan='3' width='260px'><img src='https://api.qrserver.com/v1/create-qr-code/?size=500x500&qzone=4&data=https%3A%2F%2Fcdn.a8c-ci.services%2F428e0f921ef77474515d6e3cdb7d3e0a4f1f62c9%2Fgravatar-demo-prototype-build-pr598-2569580.apk' width='250' height='250' /></td>
  <td><b>App Name</b></td><td> Gravatar Demo</td>
</tr>
<tr><td><b>Commit</b></td><td>2569580f58d9ae3853784e72a6503d204926c3a6</td></tr>
<tr><td><b>Direct Download</b></td><td><a href='https://cdn.a8c-ci.services/428e0f921ef77474515d6e3cdb7d3e0a4f1f62c9/gravatar-demo-prototype-build-pr598-2569580.apk'><code>gravatar-demo-prototype-build-pr598-2569580.apk</code></a></td></tr>
</table>

</details>

author_association: COLLABORATOR