diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml
index 18eef152..d80588ea 100644
--- a/core/common/src/main/res/values/strings.xml
+++ b/core/common/src/main/res/values/strings.xml
@@ -15,4 +15,8 @@
LCK
testtesttesttesttesttesttesttesttesttesttesttesttesttest
+
+
+ 네트워크 연결이 원활하지 않습니다
+ 알 수 없는 오류가 발생하였습니다
diff --git a/core/designsystem/src/main/java/com/teamwable/designsystem/component/indicator/WableCircularIndicator.kt b/core/designsystem/src/main/java/com/teamwable/designsystem/component/indicator/WableCircularIndicator.kt
new file mode 100644
index 00000000..83825bf4
--- /dev/null
+++ b/core/designsystem/src/main/java/com/teamwable/designsystem/component/indicator/WableCircularIndicator.kt
@@ -0,0 +1,31 @@
+package com.teamwable.designsystem.component.indicator
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.teamwable.designsystem.theme.WableTheme
+
+@Composable
+fun WableCircularIndicator(
+ modifier: Modifier = Modifier,
+ size: Dp = 32.dp,
+ color: Color = WableTheme.colors.info,
+ strokeWidth: Dp = 2.dp,
+ trackColor: Color = WableTheme.colors.progressBackground,
+) {
+ CircularProgressIndicator(
+ modifier = modifier
+ .size(size)
+ .padding(5.dp),
+ color = color,
+ strokeWidth = strokeWidth,
+ strokeCap = StrokeCap.Round,
+ trackColor = trackColor,
+ )
+}
diff --git a/core/designsystem/src/main/java/com/teamwable/designsystem/component/snackbar/WableSnackBar.kt b/core/designsystem/src/main/java/com/teamwable/designsystem/component/snackbar/WableSnackBar.kt
new file mode 100644
index 00000000..acbb9751
--- /dev/null
+++ b/core/designsystem/src/main/java/com/teamwable/designsystem/component/snackbar/WableSnackBar.kt
@@ -0,0 +1,112 @@
+package com.teamwable.designsystem.component.snackbar
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+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.res.dimensionResource
+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.teamwable.designsystem.R
+import com.teamwable.designsystem.component.indicator.WableCircularIndicator
+import com.teamwable.designsystem.extension.modifier.dropShadow
+import com.teamwable.designsystem.theme.WableTheme
+import com.teamwable.designsystem.type.SnackBarType
+
+const val SNACK_BAR_DURATION = 2000L
+
+@Composable
+fun WableSnackBar(
+ modifier: Modifier = Modifier,
+ message: String = "",
+ snackBarType: SnackBarType = SnackBarType.ERROR,
+ onClick: () -> Unit = {},
+) {
+ Box(
+ modifier = modifier
+ .padding(horizontal = 18.dp)
+ .fillMaxWidth()
+ .border(
+ width = 1.dp,
+ color = WableTheme.colors.gray200,
+ shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius_8)),
+ )
+ .dropShadow(
+ shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius_8)),
+ color = Color.Black.copy(alpha = 0.12f),
+ blur = 4.dp,
+ offsetX = 0.dp,
+ offsetY = 2.dp,
+ spread = 0.dp,
+ )
+ .background(
+ color = WableTheme.colors.gray100.copy(alpha = 0.9f),
+ shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius_8)),
+ ),
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ WableSnackBarContent(
+ snackBarType = snackBarType,
+ message = message,
+ )
+ }
+}
+
+@Composable
+fun WableSnackBarContent(
+ snackBarType: SnackBarType,
+ message: String,
+) {
+ Row(
+ modifier = Modifier
+ .padding(vertical = 8.dp, horizontal = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ when (snackBarType) {
+ SnackBarType.LOADING, SnackBarType.LOADING_PROFILE -> WableCircularIndicator()
+ else -> WableSnackBarImage(snackBarType)
+ }
+ Text(
+ text = when (snackBarType) {
+ SnackBarType.LOADING -> stringResource(id = R.string.snackbar_text_loading)
+ SnackBarType.LOADING_PROFILE -> stringResource(id = R.string.snackbar_text_profile_loading)
+ else -> message
+ },
+ textAlign = TextAlign.Start,
+ color = WableTheme.colors.black,
+ style = WableTheme.typography.body03,
+ modifier = Modifier.padding(start = 6.dp),
+ )
+ }
+}
+
+@Composable
+fun WableSnackBarImage(snackBarType: SnackBarType) {
+ Image(
+ painter = painterResource(id = snackBarType.image),
+ contentDescription = null,
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun WableButtonDialogPreview() {
+ WableTheme {
+ WableSnackBar(
+ snackBarType = SnackBarType.SUCCESS,
+ message = "프로필 사진을 5MB 이하인 사진으로 바꿔주세요",
+ )
+ }
+}
diff --git a/core/designsystem/src/main/java/com/teamwable/designsystem/component/snackbar/WableSnackBarPopUp.kt b/core/designsystem/src/main/java/com/teamwable/designsystem/component/snackbar/WableSnackBarPopUp.kt
new file mode 100644
index 00000000..6db02be4
--- /dev/null
+++ b/core/designsystem/src/main/java/com/teamwable/designsystem/component/snackbar/WableSnackBarPopUp.kt
@@ -0,0 +1,58 @@
+package com.teamwable.designsystem.component.snackbar
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+import com.teamwable.designsystem.theme.WableTheme
+import com.teamwable.designsystem.type.SnackBarType
+
+@Composable
+fun WableSnackBarPopUp(
+ isVisible: Boolean,
+ snackBarType: SnackBarType = SnackBarType.LOADING,
+ onDismissRequest: () -> Unit = {},
+) {
+ if (isVisible) {
+ Popup(
+ onDismissRequest = onDismissRequest,
+ properties = PopupProperties(
+ focusable = false,
+ dismissOnBackPress = true,
+ dismissOnClickOutside = false,
+ excludeFromSystemGesture = true,
+ ),
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(WableTheme.colors.white.copy(alpha = 0.5f)),
+ ) {
+ WableSnackBar(
+ snackBarType = snackBarType,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 30.dp),
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun WableSnackBarPopUpPreview() {
+ WableTheme {
+ WableSnackBarPopUp(
+ isVisible = true,
+ snackBarType = SnackBarType.LOADING,
+ )
+ }
+}
diff --git a/core/designsystem/src/main/java/com/teamwable/designsystem/extension/modifier/ModifierExt.kt b/core/designsystem/src/main/java/com/teamwable/designsystem/extension/modifier/ModifierExt.kt
index f497359a..f64a5259 100644
--- a/core/designsystem/src/main/java/com/teamwable/designsystem/extension/modifier/ModifierExt.kt
+++ b/core/designsystem/src/main/java/com/teamwable/designsystem/extension/modifier/ModifierExt.kt
@@ -1,5 +1,6 @@
package com.teamwable.designsystem.extension.modifier
+import android.graphics.BlurMaskFilter
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
@@ -10,7 +11,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.input.pointer.pointerInteropFilter
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -46,3 +56,35 @@ fun Modifier.noRippleDebounceClickable(
true
}
}
+
+@Composable
+fun Modifier.dropShadow(
+ shape: Shape,
+ color: Color = Color.Black.copy(alpha = 0.25f),
+ blur: Dp = 4.dp,
+ offsetY: Dp = 4.dp,
+ offsetX: Dp = 0.dp,
+ spread: Dp = 0.dp,
+) = this.drawBehind {
+ val spreadPx = spread.toPx()
+ val shadowSize = Size(size.width + spreadPx, size.height + spreadPx)
+
+ val shadowOutline = shape.createOutline(shadowSize, layoutDirection, this)
+
+ val paint = Paint().apply {
+ this.color = color
+ }
+
+ if (blur.toPx() > 0) {
+ paint.asFrameworkPaint().apply {
+ maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL)
+ }
+ }
+
+ drawIntoCanvas { canvas ->
+ canvas.save()
+ canvas.translate(offsetX.toPx() - spreadPx / 2, offsetY.toPx() - spreadPx / 2)
+ canvas.drawOutline(shadowOutline, paint)
+ canvas.restore()
+ }
+}
diff --git a/core/designsystem/src/main/java/com/teamwable/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/teamwable/designsystem/theme/Color.kt
index 829184a4..be98e899 100644
--- a/core/designsystem/src/main/java/com/teamwable/designsystem/theme/Color.kt
+++ b/core/designsystem/src/main/java/com/teamwable/designsystem/theme/Color.kt
@@ -22,6 +22,7 @@ val Warning = Color(0xFFEE9209)
val Error = Color(0xFFF01F1F)
val SystemNavigationBar = Color(0xFFF5F5F5)
val SystemLoginSystemAppBar = Color(0xFFEBE2FD)
+val ProgressBackground = Color(0xFFCADFF7)
// Gray Scale
val Black = Color(0xFF0E0E0E)
@@ -100,6 +101,7 @@ class WableColors(
kdf10: Color,
hle50: Color,
hle10: Color,
+ progressBackground: Color,
) {
var purple50 by mutableStateOf(purple50)
private set
@@ -181,6 +183,8 @@ class WableColors(
private set
var hle10 by mutableStateOf(hle10)
private set
+ var progressBackground by mutableStateOf(progressBackground)
+ private set
fun copy(): WableColors = WableColors(
purple50,
@@ -223,6 +227,7 @@ class WableColors(
kdf10,
hle50,
hle10,
+ progressBackground,
)
fun update(other: WableColors) {
@@ -266,6 +271,7 @@ class WableColors(
kdf10 = other.kdf10
hle50 = other.hle50
hle10 = other.hle10
+ progressBackground = other.progressBackground
}
}
@@ -310,6 +316,7 @@ fun wableLightColors(
kdf10: Color = Kdf10,
hle50: Color = Hle50,
hle10: Color = Hle10,
+ progressBackground: Color = ProgressBackground,
) = WableColors(
purple50,
purple100,
@@ -351,4 +358,5 @@ fun wableLightColors(
kdf10,
hle50,
hle10,
+ progressBackground,
)
diff --git a/core/designsystem/src/main/java/com/teamwable/designsystem/type/SnackBarType.kt b/core/designsystem/src/main/java/com/teamwable/designsystem/type/SnackBarType.kt
new file mode 100644
index 00000000..3aa0dae1
--- /dev/null
+++ b/core/designsystem/src/main/java/com/teamwable/designsystem/type/SnackBarType.kt
@@ -0,0 +1,16 @@
+package com.teamwable.designsystem.type
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.teamwable.designsystem.R
+
+enum class SnackBarType(
+ @DrawableRes val image: Int = com.teamwable.common.R.drawable.ic_home_toast_success,
+ @StringRes val message: Int = R.string.empty,
+) {
+ WARNING(image = com.teamwable.common.R.drawable.ic_home_toast_warning),
+ ERROR(image = com.teamwable.common.R.drawable.ic_home_toast_error),
+ SUCCESS(image = com.teamwable.common.R.drawable.ic_home_toast_success),
+ LOADING(message = R.string.snackbar_text_loading),
+ LOADING_PROFILE(message = R.string.snackbar_text_loading),
+}
diff --git a/core/designsystem/src/main/res/values/dimens.xml b/core/designsystem/src/main/res/values/dimens.xml
index 4a8b17b2..8769f704 100644
--- a/core/designsystem/src/main/res/values/dimens.xml
+++ b/core/designsystem/src/main/res/values/dimens.xml
@@ -1,4 +1,5 @@
+ 8dp
12dp
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index 52be1d24..d3151e74 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -29,4 +29,8 @@
+
+ 회원가입이 거의 마무리 되었어요.\n잠시만 기다려주세요!
+ 프로필 수정이 거의 마무리 되었어요.\n잠시만 기다려주세요!
+
diff --git a/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt b/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt
index 9b5decbb..0386bbd1 100644
--- a/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt
+++ b/core/network/src/main/java/com/teamwable/network/TokenInterceptor.kt
@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
@@ -25,6 +26,7 @@ class TokenInterceptor @Inject constructor(
private val authService: AuthService,
) : Interceptor {
private val mutex = Mutex()
+ private var currentToast: Toast? = null
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
@@ -70,17 +72,27 @@ class TokenInterceptor @Inject constructor(
}
}
- private fun handleFailedTokenReissue() = with(context) {
- CoroutineScope(Dispatchers.Main).launch {
+ private fun handleFailedTokenReissue() = CoroutineScope(Dispatchers.Main).launch {
+ showToast()
+ withContext(Dispatchers.IO) {
defaultWablePreferenceDatasource.clear()
+ }
+ restartActivity()
+ }
+
+ private fun showToast() {
+ currentToast?.cancel()
+ currentToast = Toast.makeText(context, "재 로그인이 필요해요", Toast.LENGTH_SHORT)
+ currentToast?.show()
+ }
+
+ private suspend fun restartActivity() = with(context) {
+ mutex.withLock {
startActivity(
Intent.makeRestartActivityTask(
packageManager.getLaunchIntentForPackage(packageName)?.component,
- ).apply {
- addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
- },
+ ),
)
- Toast.makeText(context, "재 로그인이 필요해요", Toast.LENGTH_SHORT).show()
}
}
diff --git a/core/ui/src/main/java/com/teamwable/ui/util/GetErrorMessage.kt b/core/ui/src/main/java/com/teamwable/ui/util/GetErrorMessage.kt
new file mode 100644
index 00000000..6524df85
--- /dev/null
+++ b/core/ui/src/main/java/com/teamwable/ui/util/GetErrorMessage.kt
@@ -0,0 +1,16 @@
+package com.teamwable.ui.util
+
+import android.content.res.Resources
+import com.teamwable.model.network.Error
+import java.net.UnknownHostException
+
+fun getErrorMessage(
+ throwable: Throwable?,
+ localContextResource: Resources,
+) = when (throwable) {
+ is UnknownHostException -> localContextResource.getString(com.teamwable.common.R.string.error_message_network)
+ is Error.NetWorkConnectError -> localContextResource.getString(com.teamwable.common.R.string.error_message_network)
+ is Error.ApiError -> throwable.message.toString()
+ is Error.TimeOutError -> throwable.message.toString()
+ else -> localContextResource.getString(com.teamwable.common.R.string.error_message_unknown)
+}
diff --git a/feature/main-compose/src/main/java/com/teamwable/main_compose/MainScreen.kt b/feature/main-compose/src/main/java/com/teamwable/main_compose/MainScreen.kt
index 0f6c568d..db12db73 100644
--- a/feature/main-compose/src/main/java/com/teamwable/main_compose/MainScreen.kt
+++ b/feature/main-compose/src/main/java/com/teamwable/main_compose/MainScreen.kt
@@ -2,6 +2,7 @@ package com.teamwable.main_compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
@@ -11,21 +12,26 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.navOptions
import com.teamwable.auth.naviagation.loginNavGraph
import com.teamwable.common.intentprovider.IntentProvider
+import com.teamwable.designsystem.component.snackbar.SNACK_BAR_DURATION
+import com.teamwable.designsystem.component.snackbar.WableSnackBar
import com.teamwable.designsystem.component.topbar.WableAppBar
+import com.teamwable.designsystem.type.SnackBarType
+import com.teamwable.main_compose.extensions.getErrorMessage
import com.teamwable.main_compose.splash.navigation.splashNavGraph
-import com.teamwable.model.network.Error
import com.teamwable.onboarding.agreeterms.naviagation.agreeTermsNavGraph
import com.teamwable.onboarding.firstlckwatch.naviagation.firstLckWatchNavGraph
import com.teamwable.onboarding.profile.naviagation.profileNavGraph
import com.teamwable.onboarding.selectlckteam.naviagation.selectLckTeamNavGraph
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import java.net.UnknownHostException
@Composable
internal fun MainScreen(
@@ -40,15 +46,11 @@ internal fun MainScreen(
val localContextResource = LocalContext.current.resources
val onShowErrorSnackBar: (throwable: Throwable?) -> Unit = { throwable ->
coroutineScope.launch {
- snackBarHostState.showSnackbar(
- when (throwable) {
- is UnknownHostException -> localContextResource.getString(R.string.error_message_network)
- is Error.NetWorkConnectError -> localContextResource.getString(R.string.error_message_network)
- is Error.ApiError -> throwable.message.toString()
- is Error.TimeOutError -> throwable.message.toString()
- else -> localContextResource.getString(R.string.error_message_unknown)
- },
- )
+ val job = launch {
+ snackBarHostState.showSnackbar(getErrorMessage(throwable, localContextResource))
+ }
+ delay(SNACK_BAR_DURATION)
+ job.cancel()
}
}
@@ -108,7 +110,19 @@ internal fun MainScreen(
)
}
}
+ SnackbarHost(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 30.dp)
+ .zIndex(1f),
+ hostState = snackBarHostState,
+ snackbar = { snackBarData ->
+ WableSnackBar(
+ message = snackBarData.visuals.message,
+ snackBarType = SnackBarType.ERROR,
+ )
+ },
+ )
},
- snackbarHost = { SnackbarHost(snackBarHostState) },
)
}
diff --git a/feature/main-compose/src/main/java/com/teamwable/main_compose/extensions/GetErrorMessage.kt b/feature/main-compose/src/main/java/com/teamwable/main_compose/extensions/GetErrorMessage.kt
new file mode 100644
index 00000000..8020cf35
--- /dev/null
+++ b/feature/main-compose/src/main/java/com/teamwable/main_compose/extensions/GetErrorMessage.kt
@@ -0,0 +1,16 @@
+package com.teamwable.main_compose.extensions
+
+import android.content.res.Resources
+import com.teamwable.model.network.Error
+import java.net.UnknownHostException
+
+fun getErrorMessage(
+ throwable: Throwable?,
+ localContextResource: Resources,
+) = when (throwable) {
+ is UnknownHostException -> localContextResource.getString(com.teamwable.common.R.string.error_message_network)
+ is Error.NetWorkConnectError -> localContextResource.getString(com.teamwable.common.R.string.error_message_network)
+ is Error.ApiError -> throwable.message.toString()
+ is Error.TimeOutError -> throwable.message.toString()
+ else -> localContextResource.getString(com.teamwable.common.R.string.error_message_unknown)
+}
diff --git a/feature/main-compose/src/main/res/values/strings.xml b/feature/main-compose/src/main/res/values/strings.xml
deleted file mode 100644
index 290814ea..00000000
--- a/feature/main-compose/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
- 네트워크 연결이 원활하지 않습니다
- 알 수 없는 오류가 발생하였습니다
-
diff --git a/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsScreen.kt b/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsScreen.kt
index fbe1a76b..ea96dc5f 100644
--- a/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsScreen.kt
+++ b/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsScreen.kt
@@ -28,11 +28,14 @@ import com.teamwable.common.util.AmplitudeUtil.trackEvent
import com.teamwable.designsystem.component.button.WableButton
import com.teamwable.designsystem.component.checkbox.WableCheckBoxWithText
import com.teamwable.designsystem.component.dialog.WableButtonDialog
+import com.teamwable.designsystem.component.snackbar.WableSnackBarPopUp
import com.teamwable.designsystem.theme.WableTheme
import com.teamwable.designsystem.type.DialogType
+import com.teamwable.designsystem.type.SnackBarType
import com.teamwable.navigation.Route
import com.teamwable.onboarding.R
import com.teamwable.onboarding.agreeterms.model.AgreeTerm
+import com.teamwable.onboarding.agreeterms.model.AgreeTermState
import com.teamwable.onboarding.agreeterms.model.AgreeTermsSideEffect
@Composable
@@ -44,6 +47,7 @@ fun AgreeTermsRoute(
) {
val lifecycleOwner = LocalLifecycleOwner.current
val showDialog by viewModel.showDialog.collectAsStateWithLifecycle()
+ val agreeTermState by viewModel.agreeTermState.collectAsStateWithLifecycle()
val memberInfoEditModel = args.memberInfoEditModel
@@ -82,6 +86,11 @@ fun AgreeTermsRoute(
onDismissRequest = { viewModel.showLoginDialog(false) },
)
}
+
+ WableSnackBarPopUp(
+ isVisible = agreeTermState is AgreeTermState.Loading,
+ snackBarType = SnackBarType.LOADING,
+ )
}
@Composable
diff --git a/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsViewModel.kt b/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsViewModel.kt
index 88a28959..20db5f8d 100644
--- a/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsViewModel.kt
+++ b/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/AgreeTermsViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.teamwable.data.repository.ProfileRepository
import com.teamwable.data.repository.UserInfoRepository
import com.teamwable.model.profile.MemberInfoEditModel
+import com.teamwable.onboarding.agreeterms.model.AgreeTermState
import com.teamwable.onboarding.agreeterms.model.AgreeTermsSideEffect
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -27,6 +28,9 @@ class AgreeTermsViewModel @Inject constructor(
private val _showDialog = MutableStateFlow(false)
val showDialog: StateFlow get() = _showDialog
+ private val _agreeTermState = MutableStateFlow(AgreeTermState.Idle)
+ val agreeTermState: StateFlow get() = _agreeTermState
+
fun navigateToHome() {
viewModelScope.launch {
_sideEffect.emit(AgreeTermsSideEffect.NavigateToHome)
@@ -39,14 +43,21 @@ class AgreeTermsViewModel @Inject constructor(
fun patchUserProfile(memberInfoEditModel: MemberInfoEditModel, imgUrl: String?) {
viewModelScope.launch {
+ _agreeTermState.update { AgreeTermState.Loading }
profileRepository.patchUserProfile(memberInfoEditModel, imgUrl)
- .onSuccess {
- _sideEffect.emit(AgreeTermsSideEffect.ShowDialog)
- userInfoRepository.saveAutoLogin(true)
- }
- .onFailure {
- _sideEffect.emit(AgreeTermsSideEffect.ShowSnackBar(it))
- }
+ .onSuccess { handleSuccess() }
+ .onFailure { handleFailure(it) }
}
}
+
+ private suspend fun handleSuccess() {
+ _agreeTermState.update { AgreeTermState.Idle }
+ _sideEffect.emit(AgreeTermsSideEffect.ShowDialog)
+ userInfoRepository.saveAutoLogin(true)
+ }
+
+ private suspend fun handleFailure(it: Throwable) {
+ _agreeTermState.update { AgreeTermState.Idle }
+ _sideEffect.emit(AgreeTermsSideEffect.ShowSnackBar(it))
+ }
}
diff --git a/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/model/AgreeTermState.kt b/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/model/AgreeTermState.kt
new file mode 100644
index 00000000..be765c33
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/teamwable/onboarding/agreeterms/model/AgreeTermState.kt
@@ -0,0 +1,13 @@
+package com.teamwable.onboarding.agreeterms.model
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+
+@Stable
+sealed interface AgreeTermState {
+ @Immutable
+ data object Idle : AgreeTermState
+
+ @Immutable
+ data object Loading : AgreeTermState
+}
diff --git a/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditFragment.kt b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditFragment.kt
index 87401626..56d6707e 100644
--- a/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditFragment.kt
+++ b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditFragment.kt
@@ -10,7 +10,6 @@ import com.teamwable.model.profile.MemberInfoEditModel
import com.teamwable.profile.R
import com.teamwable.profile.databinding.FragmentProfileEditBinding
import com.teamwable.ui.base.BindingFragment
-import com.teamwable.ui.extensions.toast
import com.teamwable.ui.util.Arg.PROFILE_EDIT_RESULT
class ProfileEditFragment : BindingFragment(FragmentProfileEditBinding::inflate) {
@@ -39,7 +38,6 @@ class ProfileEditFragment : BindingFragment(Fragment
ProfileEditRoute(
profile = profile,
navigateToProfile = { updatedProfile -> saveAndNavigateBack(updatedProfile) },
- onShowErrorSnackBar = { throwable -> toast(throwable?.message.toString()) },
)
}
}
diff --git a/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditRoute.kt b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditRoute.kt
index 44eb3331..b3c5cb37 100644
--- a/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditRoute.kt
+++ b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditRoute.kt
@@ -1,32 +1,62 @@
package com.teamwable.profile.profile.edit
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
+import com.teamwable.designsystem.component.snackbar.SNACK_BAR_DURATION
+import com.teamwable.designsystem.component.snackbar.WableSnackBar
+import com.teamwable.designsystem.component.snackbar.WableSnackBarPopUp
import com.teamwable.designsystem.type.ProfileEditType
import com.teamwable.designsystem.type.ProfileImageType
+import com.teamwable.designsystem.type.SnackBarType
import com.teamwable.model.profile.MemberInfoEditModel
import com.teamwable.onboarding.profile.ProfileScreen
import com.teamwable.onboarding.profile.model.ProfileSideEffect
import com.teamwable.onboarding.profile.permission.launchImagePicker
import com.teamwable.onboarding.profile.permission.rememberGalleryLauncher
import com.teamwable.onboarding.profile.permission.rememberPhotoPickerLauncher
+import com.teamwable.profile.profile.edit.model.ProfilePatchState
+import com.teamwable.ui.util.getErrorMessage
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
@Composable
internal fun ProfileEditRoute(
viewModel: ProfileEditViewModel = hiltViewModel(),
profile: MemberInfoEditModel,
navigateToProfile: (MemberInfoEditModel) -> Unit,
- onShowErrorSnackBar: (throwable: Throwable?) -> Unit = {},
) {
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
val profileState by viewModel.profileState.collectAsStateWithLifecycle()
+ val loadingState by viewModel.profileLoadingState.collectAsStateWithLifecycle()
+
+ val snackBarHostState = remember { SnackbarHostState() }
+ val coroutineScope = rememberCoroutineScope()
+ val localContextResource = LocalContext.current.resources
+
+ val onShowErrorSnackBar: (throwable: Throwable?) -> Unit = { throwable ->
+ coroutineScope.launch {
+ val job = launch {
+ snackBarHostState.showSnackbar(getErrorMessage(throwable, localContextResource))
+ }
+ delay(SNACK_BAR_DURATION)
+ job.cancel()
+ }
+ }
val galleryLauncher = rememberGalleryLauncher { uri ->
viewModel.onImageSelected(uri.toString())
@@ -37,7 +67,7 @@ internal fun ProfileEditRoute(
}
LaunchedEffect(Unit) {
- viewModel.onNicknameChanged(profile.nickname ?: "")
+ viewModel.onNicknameChanged(profile.nickname.orEmpty())
val profileType = ProfileImageType.entries.find { it.name == profile.memberDefaultProfileImage }
if (profileType != null) {
viewModel.onRandomImageChange(profileType)
@@ -88,4 +118,22 @@ internal fun ProfileEditRoute(
viewModel.onNicknameChanged(newNickname)
},
)
+
+ SnackbarHost(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 6.dp),
+ hostState = snackBarHostState,
+ snackbar = { snackBarData ->
+ WableSnackBar(
+ message = snackBarData.visuals.message,
+ snackBarType = SnackBarType.ERROR,
+ )
+ },
+ )
+
+ WableSnackBarPopUp(
+ isVisible = loadingState is ProfilePatchState.Loading,
+ snackBarType = SnackBarType.LOADING_PROFILE,
+ )
}
diff --git a/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditViewModel.kt b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditViewModel.kt
index b8611bcf..c5f915b4 100644
--- a/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditViewModel.kt
+++ b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/ProfileEditViewModel.kt
@@ -9,6 +9,7 @@ import com.teamwable.model.profile.MemberInfoEditModel
import com.teamwable.onboarding.profile.model.ProfileSideEffect
import com.teamwable.onboarding.profile.model.ProfileState
import com.teamwable.onboarding.profile.regex.NicknameValidationUseCase
+import com.teamwable.profile.profile.edit.model.ProfilePatchState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,6 +31,9 @@ internal class ProfileEditViewModel @Inject constructor(
private val _profileState = MutableStateFlow(ProfileState())
val profileState: StateFlow = _profileState
+ private val _profilePatchState = MutableStateFlow(ProfilePatchState.Idle)
+ val profileLoadingState: StateFlow get() = _profilePatchState
+
fun requestImagePicker() {
viewModelScope.launch {
_sideEffect.emit(ProfileSideEffect.RequestImagePicker)
@@ -71,11 +75,14 @@ internal class ProfileEditViewModel @Inject constructor(
fun patchUserProfile(memberInfoEditModel: MemberInfoEditModel, imgUrl: String?) {
viewModelScope.launch {
+ _profilePatchState.update { ProfilePatchState.Loading }
profileRepository.patchUserProfile(memberInfoEditModel, imgUrl)
.onSuccess {
+ _profilePatchState.update { ProfilePatchState.Idle }
_sideEffect.emit(ProfileSideEffect.NavigateToProfile)
}
.onFailure {
+ _profilePatchState.update { ProfilePatchState.Idle }
_sideEffect.emit(ProfileSideEffect.ShowSnackBar(it))
}
}
diff --git a/feature/profile/src/main/java/com/teamwable/profile/profile/edit/model/ProfilePatchState.kt b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/model/ProfilePatchState.kt
new file mode 100644
index 00000000..9aedce8e
--- /dev/null
+++ b/feature/profile/src/main/java/com/teamwable/profile/profile/edit/model/ProfilePatchState.kt
@@ -0,0 +1,13 @@
+package com.teamwable.profile.profile.edit.model
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+
+@Stable
+sealed interface ProfilePatchState {
+ @Immutable
+ data object Idle : ProfilePatchState
+
+ @Immutable
+ data object Loading : ProfilePatchState
+}