Skip to content
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19 changes: 12 additions & 7 deletions core/designsystem/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
<string name="profile_block_descriptioin">아직 프로필을 심사하고 있어요</string>

<!-- Onboarding -->
<string name="skip">건너뛰기</string>
<string name="start">시작하기</string>
<string name="one_day_one_matching_title">하루 한 번,\n1:1로 만나는 특별한 인연</string>
<string name="one_day_one_matching_description">매일 밤 10시, 새로운 매칭 조각이 도착해요.\n천천히 프로필을 살펴보고, 맞춰볼지 결정해보세요.</string>
<string name="camera_block_title">안심하고\n소중한 만남을 즐기세요</string>
<string name="camera_block_description">스크린샷은 제한되어 있어요.\n오직 이 공간에서만, 편안하게 인연을 찾아보세요.</string>

<string name="onboarding_basic_title">매일 밤 10시,\n가치관이 맞는 인연을\n이어줘요</string>
<string name="onboarding_basic_button">인연을 더 만나려면?</string>
<string name="onboarding_premium_title">원한다면, 퍼즐로\n더 많은 인연을\n이어갈 수 있어요</string>
<string name="onboarding_premium_button">만남은 어떻게 이루어지나요?</string>
<string name="onboarding_greenlight_title">상대가 먼저 다가오면\n‘그린라이트’,\n서로 통하면 만남이 시작돼요</string>
<string name="onboarding_greenlight_button">나와 맞는 인연을 만나려면?</string>
<string name="onboarding_talk_title">정성이 담긴 프로필은\n꼭 맞는 인연을 만날\n가능성이 높아요</string>
<string name="onboarding_talk_button">안전하게 만나고 싶어요</string>
<string name="onboarding_camera_block_title">스크린샷은 제한되어 있으니,\n안심하고 인연을 찾아보세요</string>
<string name="onboarding_camera_block_button1">다시 볼래요</string>
<string name="onboarding_camera_block_button2">시작할래요!</string>

<!-- Pause -->
<string name="pause_title">📢 잠시 쉬어가는 안내 드려요</string>
<string name="pause_description">피스를 이용해주시고, 함께해주셔서 정말 고맙습니다.\n피스의 iOS 버전 출시가 예상보다 지연되면서,\n안드로이드 서비스도 잠시 일시중지하기로 결정했습니다.</string>
Expand Down
4 changes: 4 additions & 0 deletions feature/onboarding/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ plugins {
android {
namespace = "com.puzzle.onboarding"
}

dependencies {
implementation(libs.lottie.compose)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,15 @@ package com.puzzle.onboarding

import android.app.Activity
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
Expand All @@ -30,22 +19,22 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.puzzle.analytics.TrackScreenViewEvent
import com.puzzle.common.event.PieceEvent
import com.puzzle.common.ui.SnackBarState
import com.puzzle.designsystem.R
import com.puzzle.designsystem.component.PieceOutlinedButton
import com.puzzle.designsystem.component.PieceSolidButton
import com.puzzle.designsystem.component.PieceSubCloseTopBar
import com.puzzle.designsystem.foundation.PieceTheme
import com.puzzle.onboarding.contract.OnboardingIntent
import com.puzzle.onboarding.model.OnboardingPageData
import com.puzzle.onboarding.model.onboardingPages
import com.puzzle.onboarding.ui.PageContent
import kotlinx.coroutines.launch

@Composable
Expand All @@ -59,7 +48,7 @@ internal fun OnboardingRoute(viewModel: OnboardingViewModel = hiltViewModel()) {
} else {
viewModel.eventHelper.sendEvent(
PieceEvent.ShowSnackBar(
SnackBarState.TextOnly(context.getString(com.puzzle.designsystem.R.string.back_description))
SnackBarState.TextOnly(context.getString(R.string.back_description))
)
)
}
Expand All @@ -70,211 +59,104 @@ internal fun OnboardingRoute(viewModel: OnboardingViewModel = hiltViewModel()) {
}

@Composable
internal fun OnboardingScreen(onStartButtonClick: () -> Unit) {
private fun OnboardingScreen(onStartButtonClick: () -> Unit) {
val scope = rememberCoroutineScope()
val pageCount = onboardingPages.size

val pagerState = rememberPagerState(
initialPage = 0,
pageCount = { 2 },
pageCount = { pageCount },
)
val currentPage = pagerState.currentPage
val currentPageData = onboardingPages.getOrNull(currentPage)

TrackScreenViewEvent(
key = pagerState.currentPage,
screenName = when (pagerState.currentPage) {
0 -> "onboarding_dailymatch"
1 -> "onboarding_safetynotice"
else -> null
}
)
currentPageData?.screenName?.let { screenName ->
TrackScreenViewEvent(
key = currentPage,
screenName = screenName
)
}

Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp)
.background(PieceTheme.colors.light3)
) {
OnboardingTopBar(
currentPage = pagerState.currentPage,
onSkipButtonClick = onStartButtonClick,
modifier = Modifier.padding(bottom = 49.dp),

PieceSubCloseTopBar(
title = "",
contentColor = PieceTheme.colors.black,
onCloseClick = onStartButtonClick,
modifier = Modifier.padding(horizontal = 20.dp)
)

HorizontalPager(
state = pagerState,
modifier = Modifier
.weight(1f)
.padding(bottom = 40.dp)
) { page ->
Column(modifier = Modifier.fillMaxSize()) {
when (page) {
0 -> OnboardingPageContent(
imageRes = R.drawable.ic_onboarding_matching,
title = stringResource(R.string.one_day_one_matching_title),
description = stringResource(R.string.one_day_one_matching_description),
)
userScrollEnabled = false,
modifier = Modifier.weight(1f)
) { pageIndex ->

val isCurrentPage = pageIndex == currentPage

1 -> OnboardingPageContent(
imageRes = R.drawable.ic_onboarding_camera,
title = stringResource(R.string.camera_block_title),
description = stringResource(R.string.camera_block_description),
)
}
onboardingPages.getOrNull(pageIndex)?.let { data ->
PageContent(
titleRes = data.titleRes,
pageContentType = data.contentType,
isPageActive = isCurrentPage
)
}
}

OnboardingIndicator(
total = 2,
current = pagerState.currentPage,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 30.dp),
)

PieceSolidButton(
label = when (pagerState.currentPage) {
1 -> stringResource(R.string.start)
else -> stringResource(R.string.next)
},
onClick = {
when (pagerState.currentPage) {
1 -> onStartButtonClick()
else -> scope.launch { pagerState.animateScrollToPage(1) }
}
},
modifier = Modifier
.padding(bottom = 10.dp, top = 12.dp)
.fillMaxWidth(),
OnboardingBottomButton(
currentPage = currentPage,
pageCount = pageCount,
currentPageData = currentPageData,
onNextClick = { scope.launch { pagerState.scrollToPage(currentPage + 1) } },
onStartClick = onStartButtonClick,
onRestartClick = { scope.launch { pagerState.scrollToPage(0) } }
)
}
}

@Composable
private fun OnboardingTopBar(
private fun OnboardingBottomButton(
currentPage: Int,
onSkipButtonClick: () -> Unit,
modifier: Modifier = Modifier,
pageCount: Int,
currentPageData: OnboardingPageData?,
onNextClick: () -> Unit,
onStartClick: () -> Unit,
onRestartClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.height(64.dp),
) {
Image(
painter = painterResource(R.drawable.ic_onboarding_logo),
contentDescription = null,
)

Spacer(modifier = Modifier.weight(1f))

AnimatedVisibility(
visible = currentPage == 0,
enter = fadeIn(),
exit = fadeOut(),
if (currentPage == pageCount - 1) {
Row(
modifier = Modifier
.padding(horizontal = 20.dp)
.padding(bottom = 10.dp, top = 12.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.skip),
style = PieceTheme.typography.bodyMM.copy(
textDecoration = TextDecoration.Underline
),
color = PieceTheme.colors.dark3,
modifier = Modifier.clickable { onSkipButtonClick() }
PieceOutlinedButton(
label = stringResource(R.string.onboarding_camera_block_button1),
onClick = onRestartClick,
modifier = Modifier.weight(1f)
)
PieceSolidButton(
label = stringResource(R.string.onboarding_camera_block_button2),
onClick = onStartClick,
modifier = Modifier.weight(1f)
)
}
}
}
} else {
val labelRes = currentPageData?.buttonLabelRes ?: return

@Composable
private fun OnboardingPageContent(
imageRes: Int,
title: String,
description: String
) {
Column(modifier = Modifier.fillMaxSize()) {
Image(
painter = painterResource(imageRes),
contentDescription = null,
PieceSolidButton(
label = stringResource(labelRes),
onClick = onNextClick,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 66.dp),
)

Text(
text = title,
textAlign = TextAlign.Start,
style = PieceTheme.typography.headingLSB,
color = PieceTheme.colors.black,
modifier = Modifier.padding(bottom = 12.dp),
)

Text(
text = description,
textAlign = TextAlign.Start,
style = PieceTheme.typography.bodySM,
color = PieceTheme.colors.dark3,
)
}
}

@Composable
private fun OnboardingIndicator(
total: Int,
current: Int,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
(0 until total).forEachIndexed { index, _ ->
if (index == current) {
Spacer(
modifier = Modifier
.size(width = 20.dp, height = 8.dp)
.clip(CircleShape)
.background(PieceTheme.colors.dark2)
)
} else {
Spacer(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(PieceTheme.colors.light1)
)
}
}
}
}

@Preview
@Composable
private fun OnboardingScreenPreview() {
PieceTheme {
Surface(
color = PieceTheme.colors.white,
modifier = Modifier.fillMaxSize(),
) {
OnboardingScreen {}
}
}
}

@Preview
@Composable
private fun OnboardingTopBarPreview() {
PieceTheme {
OnboardingTopBar(
currentPage = 0,
onSkipButtonClick = {}
.padding(horizontal = 20.dp)
.padding(bottom = 10.dp, top = 12.dp)
.fillMaxWidth(),
)
}
}


@Preview
@Composable
private fun OnboardingIndicatorPreview() {
PieceTheme {
OnboardingIndicator(total = 2, current = 0)
}
}
}
Loading