diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc35c6cb..60450c72 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("org.jetbrains.kotlin.android") id("kotlin-kapt") id("com.google.firebase.crashlytics") + id("com.google.android.gms.oss-licenses-plugin") } android { @@ -53,6 +54,7 @@ dependencies { implementation(project(":data")) implementation(libs.bundles.android.base) + implementation(libs.play.services.oss.licenses) val composeBom = platform("androidx.compose:compose-bom:2024.05.00") implementation(composeBom) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cec7c47f..0b6a35bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,12 @@ + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 42c7e3c3..ae0f17bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.google.android.gms:oss-licenses-plugin:0.10.6") + } +} + plugins { id("com.android.application") version "7.4.2" apply false id("com.android.library") version "7.4.2" apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 331d51f4..87f9d9a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ android-material = "1.8.0" junit = "4.13.2" androidx-test-junit = "1.1.5" espresso-core = "3.5.1" +play-services-oss-licenses = "17.1.0" [libraries] androidx-navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } @@ -34,6 +35,7 @@ hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-complier = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } javax-inject = { module = "javax.inject:javax.inject", version.ref = "javax-inject" } +play-services-oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "play-services-oss-licenses" } [bundles] navigation = ["androidx-navigation-fragment-ktx", "androidx-navigation-ui-ktx", "androidx-navigation-dynamic-features-fragment"] diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 701595be..4fac0651 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(project(":domain")) implementation(libs.bundles.android.base) + implementation(libs.play.services.oss.licenses) val composeBom = platform("androidx.compose:compose-bom:2024.05.00") implementation(composeBom) diff --git a/presentation/src/main/java/com/whyranoid/presentation/reusable/MenuItem.kt b/presentation/src/main/java/com/whyranoid/presentation/reusable/MenuItem.kt new file mode 100644 index 00000000..fa7d635c --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/reusable/MenuItem.kt @@ -0,0 +1,81 @@ +package com.whyranoid.presentation.reusable + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.whyranoid.presentation.R +import com.whyranoid.presentation.theme.WalkieColor +import com.whyranoid.presentation.theme.WalkieTheme +import com.whyranoid.presentation.theme.WalkieTypography + +@Composable +fun MenuItem( + @StringRes text: Int, + subInfo: String?= null, + @DrawableRes icon: Int? = null, + onClick: () -> Unit = {} +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + .padding(horizontal = 20.dp) + .clickable(onClick = onClick) + ) { + if (icon != null) { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + } + + Text( + text = stringResource(id = text), + style = WalkieTypography.Body1_Normal + ) + + if (subInfo != null) { + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = subInfo, + style = WalkieTypography.Body1_Normal, + color = WalkieColor.Primary, + textAlign = TextAlign.End, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Preview +@Composable +private fun MenuItemPreview() { + WalkieTheme { + MenuItem( + text = R.string.version_info, + subInfo = "1.0.0", + icon = R.drawable.ic_mobile + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt index 1e73ace1..ca1f9a46 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt @@ -1,5 +1,6 @@ package com.whyranoid.presentation.screens +import android.util.Log import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding @@ -253,6 +254,14 @@ fun AppScreenContent( } ) } + + composable( + route = Screen.WebViewScreen.route, + arguments = Screen.WebViewScreen.arguments, + ) { + val url = it.arguments?.getString("url") + WebViewScreen(url = url) + } } val isLoading = ApiResponseDialog.isLoading.collectAsStateWithLifecycle() diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt index ade74262..c2be8c7b 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt @@ -6,6 +6,8 @@ import androidx.navigation.NamedNavArgument import androidx.navigation.NavType import androidx.navigation.navArgument import com.whyranoid.presentation.R +import java.net.URLEncoder +import java.nio.charset.StandardCharsets sealed class Screen( val route: String, @@ -143,6 +145,21 @@ sealed class Screen( fun route(uid: Long, postId: Long) = "userPostsScreen/$uid/$postId" } + object WebViewScreen : Screen( + route = "webViewScreen/{$URL}", + arguments = listOf( + navArgument(URL) { + type = NavType.StringType + nullable = true + } + ), + ) { + fun createRoute(url: String): String { + val encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.toString()) + return route.replace("{$URL}", encodedUrl) + } + } + companion object { val bottomNavigationItems = listOf(Running, Community, ChallengeMainScreen, MyPage) @@ -151,5 +168,6 @@ sealed class Screen( const val IS_FOLLOWING_ARGUMENT = "isFollowing" const val PAGE_NO = "pageNo" const val POST_ID = "postId" + const val URL = "url" } } diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/WebViewScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/WebViewScreen.kt new file mode 100644 index 00000000..dadfb3c6 --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/WebViewScreen.kt @@ -0,0 +1,43 @@ +package com.whyranoid.presentation.screens + +import android.annotation.SuppressLint +import android.graphics.Color +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun WebViewScreen( + url: String?, +) { + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + factory = { context -> + WebView(context).apply { + setBackgroundColor(Color.TRANSPARENT) + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + webViewClient = WebViewClient() + settings.apply { + javaScriptEnabled = true + loadWithOverviewMode = true + useWideViewPort = true + domStorageEnabled = true + setSupportZoom(false) + } + } + }, + update = { webView -> + webView.loadUrl(url ?: "") + } + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt index 6492deee..66effc2e 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt @@ -1,5 +1,8 @@ package com.whyranoid.presentation.screens.setting +import android.content.Intent +import android.net.Uri +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,32 +20,39 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.getString import androidx.navigation.NavHostController import coil.compose.AsyncImage -import com.whyranoid.domain.model.user.User +import com.google.android.gms.oss.licenses.OssLicensesMenuActivity +import com.whyranoid.presentation.R +import com.whyranoid.presentation.reusable.MenuItem import com.whyranoid.presentation.screens.Screen +import com.whyranoid.presentation.screens.mypage.editprofile.UserInfoUiState +import com.whyranoid.presentation.theme.SystemColor import com.whyranoid.presentation.theme.WalkieColor import com.whyranoid.presentation.theme.WalkieTypography +import com.whyranoid.presentation.util.AppVersionUtil import org.koin.androidx.compose.koinViewModel @Composable fun SettingsScreen(navHostController: NavHostController) { val viewModel = koinViewModel() - val user = viewModel.user.collectAsState() + val user = viewModel.userInfoUiState.collectAsState() val scrollState = rememberScrollState() @@ -55,7 +65,11 @@ fun SettingsScreen(navHostController: NavHostController) { TopAppBar { navHostController.popBackStack() } ProfileSection(it) { navHostController.navigate(Screen.EditProfileScreen.route) } - SettingsList() + SettingsList( + navigateToInAppBrowser = { url -> + navHostController.navigate(Screen.WebViewScreen.createRoute(url)) + } + ) } } } @@ -86,7 +100,7 @@ fun TopAppBar( @Composable fun ProfileSection( - user: User, + user: UserInfoUiState, onProfileEditClicked: () -> Unit, ) { Row( @@ -97,7 +111,7 @@ fun ProfileSection( verticalAlignment = Alignment.CenterVertically ) { AsyncImage( - model = user.imageUrl, + model = user.profileUrl ?: R.drawable.ic_default_profile, contentDescription = "Profile Image", contentScale = ContentScale.Crop, modifier = Modifier @@ -106,10 +120,21 @@ fun ProfileSection( ) Spacer(modifier = Modifier.width(16.dp)) Column { - Text(text = user.nickname, style = MaterialTheme.typography.subtitle1) - Text(text = user.name, style = MaterialTheme.typography.body2) + Text( + text = user.nickname, + style = WalkieTypography.SubTitle + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = user.name, + style = WalkieTypography.Body2, + color = SystemColor.Negative + ) } } + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, @@ -124,7 +149,7 @@ fun ProfileSection( .padding(12.dp) ) { Text( - text = "프로필 편집", + text = stringResource(id = R.string.edit_profile), modifier = Modifier .fillMaxWidth(), textAlign = TextAlign.Center, @@ -132,56 +157,72 @@ fun ProfileSection( ) Spacer(modifier = Modifier.height(16.dp)) } + + Spacer(modifier = Modifier.height(12.dp)) } @Composable -fun SettingsList() { - val settingsGroups = listOf( - "내 정보" to listOf("내 계정", "내 신체 정보"), - "소셜" to listOf("프로필 공개 설정", "친구 관리"), - "앱 설정" to listOf("러닝 설정", "알림 설정", "푸시 설정"), - "앱 관리" to listOf("고객센터", "라이센스", "이용약관 및 운영정책", "버전 정보") +fun SettingsList( + navigateToInAppBrowser: (url: String) -> Unit = {}, +) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .background(WalkieColor.GrayBackground) ) + GroupTitle(title = R.string.manage_app) - settingsGroups.forEach { (groupTitle, items) -> - Spacer(modifier = Modifier.height(8.dp)) - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(8.dp) - .background(WalkieColor.GrayBackground) - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = groupTitle, - style = WalkieTypography.Body1_SemiBold, - modifier = Modifier - .padding(vertical = 8.dp) - .padding(horizontal = 20.dp) - ) - items.forEach { item -> - SettingsItem(text = item) - } + val context = LocalContext.current + val emailAddress = "lets.walkie@gmail.com" + val emailTitle = stringResource(id = R.string.customer_service_email_title) + val emailContent = stringResource(id = R.string.customer_service_email_content) + val emailAppChooserTitle = stringResource(id = R.string.customer_service_email_chooser) + + val emailIntent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(emailAddress)) + putExtra(Intent.EXTRA_SUBJECT, emailTitle) + putExtra(Intent.EXTRA_TEXT, emailContent) } + MenuItem( + text = R.string.customer_service, + icon = R.drawable.ic_call, + onClick = { + context.startActivity( + Intent.createChooser(emailIntent, emailAppChooserTitle) + ) + }, + ) - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(8.dp) - .background(WalkieColor.GrayBackground) + OssLicensesMenuActivity.setActivityTitle(getString(context, R.string.open_source_license)) + val ossLicensesIntent = Intent(context, OssLicensesMenuActivity::class.java) + + MenuItem( + text = R.string.license, + icon = R.drawable.ic_info, + onClick = { + context.startActivity(ossLicensesIntent) + }, ) - Text( - text = "로그아웃", - style = MaterialTheme.typography.button, - modifier = Modifier - .padding(vertical = 16.dp) - .padding(horizontal = 20.dp), - textAlign = TextAlign.Center + MenuItem( + text = R.string.terms_and_policy, + icon = R.drawable.ic_book, + onClick = { + navigateToInAppBrowser("https://festive-recorder-88c.notion.site/140d4d2df2e2800ca1b6f35c7a1043fc?pvs=4") + } ) + val versionName = AppVersionUtil.getVersionName(context) + + MenuItem( + text = R.string.version_info, + subInfo = versionName.orEmpty(), + icon = R.drawable.ic_mobile, + ) Spacer( modifier = Modifier @@ -190,14 +231,10 @@ fun SettingsList() { .background(WalkieColor.GrayBackground) ) - Text( - text = "계정 삭제", - style = MaterialTheme.typography.button, - modifier = Modifier - .padding(vertical = 16.dp) - .padding(horizontal = 20.dp), - textAlign = TextAlign.Center - ) + MenuItem( + text = R.string.logout, + icon = null + ) Spacer( modifier = Modifier @@ -205,22 +242,22 @@ fun SettingsList() { .height(8.dp) .background(WalkieColor.GrayBackground) ) + + MenuItem( + text = R.string.delete_account, + icon = null + ) } @Composable -fun SettingsItem(text: String) { - Row( +private fun GroupTitle( + @StringRes title: Int +) { + Text( + text = stringResource(id = title), + style = WalkieTypography.Body1_SemiBold, modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp) - .padding(horizontal = 20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = text, style = MaterialTheme.typography.body1) - Spacer(modifier = Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.KeyboardArrowRight, - contentDescription = "Navigate" - ) - } + .padding(start = 16.dp, top = 12.dp, bottom = 8.dp) + ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingViewModel.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingViewModel.kt index 28267e22..a7bcec25 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingViewModel.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingViewModel.kt @@ -2,26 +2,36 @@ package com.whyranoid.presentation.screens.setting import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.whyranoid.domain.model.user.User +import com.whyranoid.domain.repository.AccountRepository import com.whyranoid.domain.usecase.GetMyUidUseCase -import com.whyranoid.domain.usecase.GetUserUseCase +import com.whyranoid.presentation.screens.mypage.editprofile.UserInfoUiState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class SettingViewModel( + private val accountRepository: AccountRepository, getMyUidUseCase: GetMyUidUseCase, - getUserUseCase: GetUserUseCase, ): ViewModel() { - private val _user = MutableStateFlow(null) - val user: StateFlow = _user.asStateFlow() + private val _userInfoUiState = MutableStateFlow(null) + val userInfoUiState: StateFlow = _userInfoUiState.asStateFlow() init { viewModelScope.launch { - val uid = getMyUidUseCase().getOrNull() - val user = uid?.let { getUserUseCase(it).getOrNull() } - _user.value = user + val uid = getMyUidUseCase().getOrNull() ?: return@launch + + accountRepository.getUserInfo(uid) + .onSuccess { userInfo -> + _userInfoUiState.update { + UserInfoUiState( + userInfo.name, + userInfo.nickname, + userInfo.profileImg + ) + } + } } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/util/AppVersionUtil.kt b/presentation/src/main/java/com/whyranoid/presentation/util/AppVersionUtil.kt new file mode 100644 index 00000000..d2d9e15a --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/util/AppVersionUtil.kt @@ -0,0 +1,13 @@ +package com.whyranoid.presentation.util + +import android.content.Context + +object AppVersionUtil { + fun getVersionName(context: Context): String? { + return runCatching { + context.packageManager + .getPackageInfo(context.packageName, 0) + .versionName + }.getOrNull() + } +} \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_book.xml b/presentation/src/main/res/drawable/ic_book.xml new file mode 100644 index 00000000..1840dacb --- /dev/null +++ b/presentation/src/main/res/drawable/ic_book.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_call.xml b/presentation/src/main/res/drawable/ic_call.xml new file mode 100644 index 00000000..7a0302fe --- /dev/null +++ b/presentation/src/main/res/drawable/ic_call.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/presentation/src/main/res/drawable/ic_info.xml b/presentation/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..0cdad982 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_info.xml @@ -0,0 +1,17 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_mobile.xml b/presentation/src/main/res/drawable/ic_mobile.xml new file mode 100644 index 00000000..463abea7 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_mobile.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 61c1a0aa..f37c9213 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -10,4 +10,18 @@ %d 개 획득 + + 프로필 편집 + 앱 관리 + 고객센터 + 라이센스 + 개인정보 처리방침 + 버전 정보 + 로그아웃 + 계정 삭제 + 사용자 문의 사항 + 문제가 발생한 화면을 함께 첨부해주시면 더욱 빠른 처리가 가능합니다. 저희 서비스를 이용해주셔서 감사합니다. + 이메일 앱을 선택해주세요 + 오픈소스 라이센스 + \ No newline at end of file