Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.
Merged
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
1 change: 1 addition & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
28 changes: 28 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
id("java-library")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.ksp)
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17

dependencies {
implementation(project(":foundations"))

implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines)
implementation(libs.retrofit)
implementation(libs.retrofit.serialization)
ksp(libs.moshi.kotlin.codegen)

// Test dependencies
testImplementation(libs.junit)
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
Empty file added api/consumer-rules.pro
Empty file.
21 changes: 21 additions & 0 deletions api/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions api/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
27 changes: 27 additions & 0 deletions api/src/main/kotlin/com/gravatar/app/di/ApiModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.gravatar.app.di

import com.gravatar.app.restapi.GravatarApi
import com.gravatar.app.services.GravatarService
import com.gravatar.app.services.GravatarServiceImpl
import com.squareup.moshi.Moshi
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

val apiModule = module {
single<Retrofit> {
Retrofit.Builder()
.baseUrl("https://api.gravatar.com/v3/")
.addConverterFactory(
MoshiConverterFactory.create()
)
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Retrofit instance is created without proper HTTP client configuration. Consider using the existing OkHttpClient from the userComponent module to maintain consistent networking configuration including interceptors and timeouts.

Suggested change
)
)
.client(get())

Copilot uses AI. Check for mistakes.
.build()
}
single<GravatarApi> { get<Retrofit>().create(GravatarApi::class.java) }
single<GravatarService> {
GravatarServiceImpl(
gravatarApi = get(),
dispatchers = get()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gravatar.app.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class DeleteAccountStatus(
@Json(name = "status") val status: String = "disabled",
)
24 changes: 24 additions & 0 deletions api/src/main/kotlin/com/gravatar/app/restapi/GravatarApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.gravatar.app.restapi

import com.gravatar.app.model.DeleteAccountStatus
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST

/**
* Interface defining the Gravatar API endpoints.
*/
internal interface GravatarApi {
/**
* Disables the user's Gravatar account.
* Makes a POST request to https://api.gravatar.com/v3/me/status
*
* @return [Result] indicating success or failure of the operation
*/
@POST("me/status")
suspend fun disableAccount(
@Header("Authorization") authorization: String,
@Body body: DeleteAccountStatus = DeleteAccountStatus()
): Response<Unit>
}
29 changes: 29 additions & 0 deletions api/src/main/kotlin/com/gravatar/app/services/GravatarService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.gravatar.app.services

import com.gravatar.app.foundations.DispatcherProvider
import com.gravatar.app.restapi.GravatarApi
import kotlinx.coroutines.withContext

internal class GravatarServiceImpl(
private val gravatarApi: GravatarApi,
private val dispatchers: DispatcherProvider
) : GravatarService {

override suspend fun deleteProfile(authorization: String): Result<Unit> = withContext(dispatchers.io) {
return@withContext try {
val response = gravatarApi.disableAccount("Bearer $authorization")

if (response.isSuccessful) {
Result.success(Unit)
} else {
Result.failure(Exception("Failed to disable account."))
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "Failed to disable account." is generic and doesn't include HTTP status code or response details. Consider providing more specific error information like "Failed to disable account: HTTP ${response.code()}" to help with debugging.

Suggested change
Result.failure(Exception("Failed to disable account."))
Result.failure(
Exception(
"Failed to disable account: HTTP ${response.code()} - ${response.message()}${response.errorBody()?.let { " - ${it.string()}" } ?: ""}"
)
)

Copilot uses AI. Check for mistakes.
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

interface GravatarService {
suspend fun deleteProfile(authorization: String): Result<Unit>
}
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ kotlin = "2.1.21"
lifecycleRuntimeKtx = "2.9.1"
navigationCompose = "2.9.0"
mockk = "1.14.2"
retrofit = "3.0.0"
roborazzi = "1.45.1"
robolectric = "4.14.1"
room = "2.7.2"
Expand All @@ -27,6 +28,7 @@ ucrop = "2.2.11"
androidxTestCore = "1.6.1"
constraintLayout = "1.1.0"
qrose= "1.0.1"
moshi = "1.15.2"

[libraries]
kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinCoroutines" }
Expand Down Expand Up @@ -72,6 +74,9 @@ ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor"
ktor-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
moshi-kotlin-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-serialization = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.gravatar.app.homeUi.presentation.DrawableUtils
import com.gravatar.app.homeUi.presentation.FileUtils
import com.gravatar.app.homeUi.presentation.home.HomeViewModel
import com.gravatar.app.homeUi.presentation.home.components.topbar.TopBarPickerPopupViewModel
import com.gravatar.app.homeUi.presentation.home.components.topbar.components.about.AboutAppDialogViewModel
import com.gravatar.app.homeUi.presentation.home.gravatar.GravatarViewModel
import com.gravatar.app.homeUi.presentation.home.profile.ProfileViewModel
import com.gravatar.app.homeUi.presentation.home.share.ShareViewModel
Expand All @@ -21,6 +22,7 @@ val homeUiModule = module {
viewModelOf(::ProfileViewModel)
viewModelOf(::GravatarViewModel)
viewModelOf(::TopBarPickerPopupViewModel)
viewModelOf(::AboutAppDialogViewModel)
viewModelOf(::ShareViewModel)
viewModelOf(::HomeViewModel)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
package com.gravatar.app.homeUi.presentation.home.components.topbar.components
package com.gravatar.app.homeUi.presentation.home.components.topbar.components.about

import android.content.Context
import android.content.Intent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
Expand All @@ -26,30 +39,75 @@ import com.gravatar.app.design.components.dialog.GravatarDialog
import com.gravatar.app.design.theme.GravatarAppTheme
import com.gravatar.app.homeUi.AppVersion
import com.gravatar.app.homeUi.R
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AboutAppDialog(
onDismissRequest: () -> Unit,
viewModel: AboutAppDialogViewModel = koinViewModel()
) {
val appVersion: AppVersion = koinInject()
val uiState by viewModel.uiState.collectAsState()

GravatarDialog(
onDismissRequest = onDismissRequest,
content = {
AboutAppDialogContent(
appVersion = appVersion.value,
onDone = onDismissRequest,
modifier = Modifier
)
if (uiState.isLoading) {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onSurfaceVariant,
strokeWidth = 4.dp,
modifier = Modifier.padding(16.dp)
)
}
} else {
AboutAppDialogContent(
appVersion = appVersion.value,
onDone = onDismissRequest,
onEvent = viewModel::onEvent,
modifier = Modifier
)
}
if (uiState.showDeleteAccountErrorAlert) {
BasicAlertDialog(
onDismissRequest = { viewModel.dismissErrorMessage() }
) {
Surface(
modifier = Modifier.wrapContentWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = stringResource(R.string.about_app_dialog_delete_profile_error_message))
Spacer(modifier = Modifier.height(24.dp))
TextButton(
onClick = { viewModel.dismissErrorMessage() },
modifier = Modifier.align(Alignment.End)
) {
Text(text = stringResource(R.string.done_button_cta))
}
}
}
}
}
}
)

if (uiState.isDeleteConfirmationVisible) {
DeleteConfirmationBottomSheet(
onDismiss = { viewModel.onEvent(AboutAppDialogEvent.OnHideDeleteConfirmation) },
onConfirm = { viewModel.onEvent(AboutAppDialogEvent.OnConfirmDeleteAccount) }
)
}
}

@Composable
internal fun AboutAppDialogContent(
appVersion: String,
onDone: () -> Unit,
onEvent: (AboutAppDialogEvent) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
Expand Down Expand Up @@ -109,6 +167,28 @@ internal fun AboutAppDialogContent(
}
)
}
Column {
Text(
text = stringResource(R.string.about_app_dialog_delete_account),
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.SemiBold
),
modifier = modifier.padding(top = 4.dp),
)
DialogText(
text = stringResource(R.string.about_app_dialog_delete_profile_description),
)
Text(
text = stringResource(R.string.about_app_dialog_delete_account_button),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier
.padding(top = 16.dp, bottom = 8.dp)
.clickable {
onEvent(AboutAppDialogEvent.OnShowDeleteConfirmation)
}
)
}
PrimaryButton(
text = stringResource(R.string.done_button_cta),
onClick = onDone,
Expand Down Expand Up @@ -150,7 +230,8 @@ private fun AboutAppDialogContentPreview() {
AboutAppDialogContent(
appVersion = "0.0.1",
onDone = { },
modifier = Modifier.fillMaxWidth(),
onEvent = { _ -> },
modifier = Modifier.fillMaxWidth()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gravatar.app.homeUi.presentation.home.components.topbar.components.about

sealed class AboutAppDialogEvent {
data object OnShowDeleteConfirmation : AboutAppDialogEvent()
data object OnHideDeleteConfirmation : AboutAppDialogEvent()
data object OnConfirmDeleteAccount : AboutAppDialogEvent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gravatar.app.homeUi.presentation.home.components.topbar.components.about

internal data class AboutAppDialogState(
val isDeleteConfirmationVisible: Boolean = false,
val isLoading: Boolean = false,
val showDeleteAccountErrorAlert: Boolean = false,
)
Loading