Skip to content

Commit

Permalink
Merge pull request #242 from Nexters/feature/Boolti-227
Browse files Browse the repository at this point in the history
Boolti-227 feat: 구매 중 품절 케이스 구현
  • Loading branch information
mangbaam authored May 6, 2024
2 parents 19739b1 + 87ee687 commit fb4455c
Show file tree
Hide file tree
Showing 17 changed files with 161 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.nexters.boolti.data.datasource
import com.nexters.boolti.data.network.api.TicketingService
import com.nexters.boolti.data.network.request.ReservationInviteTicketRequest
import com.nexters.boolti.data.network.request.ReservationSalesTicketRequest
import com.nexters.boolti.data.network.response.ApprovePaymentResponse
import com.nexters.boolti.data.network.response.CheckInviteCodeResponse
import com.nexters.boolti.domain.model.ApprovePaymentResponse
import com.nexters.boolti.domain.model.TicketWithQuantity
import com.nexters.boolti.domain.model.TicketingInfo
import com.nexters.boolti.domain.request.CheckInviteCodeRequest
Expand Down Expand Up @@ -53,8 +53,8 @@ internal class TicketingDataSource @Inject constructor(
return ticketingService.requestOrderId(request).orderId
}

suspend fun approvePayment(request: PaymentApproveRequest): ApprovePaymentResponse {
return ticketingService.approvePayment(request).toDomain()
suspend fun approvePayment(request: PaymentApproveRequest): Response<ApprovePaymentResponse> {
return ticketingService.approvePayment(request)
}

suspend fun cancelPayment(request: PaymentCancelRequest): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ internal interface TicketingService {
@POST("/app/api/v1/order/approve-payment")
suspend fun approvePayment(
@Body request: PaymentApproveRequest,
): ApprovePaymentResponse
): Response<ApprovePaymentResponse>

@POST("/app/api/v1/order/cancel-payment")
suspend fun cancelPayment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.nexters.boolti.data.repository
import com.nexters.boolti.data.datasource.ReservationDataSource
import com.nexters.boolti.data.datasource.TicketingDataSource
import com.nexters.boolti.data.network.request.toData
import com.nexters.boolti.domain.exception.TicketingErrorType
import com.nexters.boolti.domain.exception.TicketingException
import com.nexters.boolti.domain.extension.errorType
import com.nexters.boolti.domain.model.ApprovePaymentResponse
import com.nexters.boolti.domain.model.InviteCodeStatus
Expand Down Expand Up @@ -65,7 +67,13 @@ internal class TicketingRepositoryImpl @Inject constructor(
}

override fun approvePayment(request: PaymentApproveRequest): Flow<ApprovePaymentResponse> = flow {
emit(dataSource.approvePayment(request))
val response = dataSource.approvePayment(request)
if (response.isSuccessful) {
response.body()?.let { emit(it.toDomain()) }
} else {
val errMsg = response.errorBody()?.string()
throw TicketingException(TicketingErrorType.fromString(errMsg?.errorType))
}
}

override fun cancelPayment(request: PaymentCancelRequest): Flow<Boolean> = flow {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.nexters.boolti.domain.exception

data class TicketingException(
val errorType: TicketingErrorType?,
) : Exception(errorType?.name)

enum class TicketingErrorType {
Unknown, NoRemainingQuantity, ApprovePaymentFailed;

companion object {
fun fromString(type: String?) = when (type?.trim()?.uppercase()) {
"NO_REMAINING_QUANTITY" -> NoRemainingQuantity
"APPROVE_PAYMENT_FAILED" -> ApprovePaymentFailed
else -> Unknown
}
}
}
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[versions]
minSdk = "26"
targetSdk = "34"
versionCode = "8"
versionName = "1.4.0"
versionCode = "9"
versionName = "1.5.0"
packageName = "com.nexters.boolti"
compileSdk = "34"
targetJvm = "17"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName:
TicketingScreen(
modifier = modifier,
navigateTo = navController::navigateTo,
popBackStack = navController::popBackStack
popBackStack = navController::popBackStack,
)
QrFullScreen(modifier = modifier, popBackStack = navController::popBackStack)
HostedShowScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,12 @@ private fun EmptyContent(
) {
Text(
text = stringResource(id = R.string.reservations_empty),
style = MaterialTheme.typography.titleLarge.copy(color = Grey05),
style = MaterialTheme.typography.headlineSmall.copy(color = Grey05),
)
Text(
modifier = Modifier.padding(top = subTextPadding),
text = stringResource(id = R.string.reservations_empty_sub),
style = MaterialTheme.typography.titleLarge.copy(color = Grey30),
style = MaterialTheme.typography.bodyLarge.copy(color = Grey30),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
Expand Down Expand Up @@ -71,6 +72,10 @@ fun ChooseTicketBottomSheet(
val uiState by viewModel.uiState.collectAsState()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)

LaunchedEffect(Unit) {
viewModel.load()
}

ModalBottomSheet(
onDismissRequest = {
onDismissRequest()
Expand Down Expand Up @@ -222,7 +227,7 @@ private fun ChooseTicketBottomSheetContent2(
)
Spacer(modifier = Modifier.weight(1F))
Text(
text = stringResource(R.string.format_total_price, ticket.ticket.price * ticketCount),
text = stringResource(R.string.format_price, ticket.ticket.price * ticketCount),
style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.primary),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.nexters.boolti.presentation.screen.ticketing

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.nexters.boolti.presentation.R
import com.nexters.boolti.presentation.component.BTDialog
import com.nexters.boolti.presentation.theme.BooltiTheme
import com.nexters.boolti.presentation.theme.Grey50

@Composable
fun PaymentFailureDialog(modifier: Modifier = Modifier, onClickButton: () -> Unit) {
BTDialog(
modifier = modifier,
enableDismiss = false,
showCloseButton = false,
positiveButtonLabel = stringResource(R.string.ticketing_again),
onClickPositiveButton = onClickButton,
) {
Text(
text = stringResource(R.string.payment_failed),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
modifier = Modifier.padding(top = 4.dp),
text = stringResource(R.string.payment_failed_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall,
color = Grey50,
)
}
}

@Preview
@Composable
private fun PaymentFailureDialogPreview() {
BooltiTheme {
Surface {
PaymentFailureDialog {}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.nexters.boolti.domain.request.SalesTicketRequest
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
Expand All @@ -28,16 +27,12 @@ class SalesTicketViewModel @Inject constructor(
private val _uiState = MutableStateFlow(SalesTicketState())
val uiState = _uiState.asStateFlow()

init {
load()
}

private fun load() {
fun load() {
viewModelScope.launch {
repository.getSalesTickets(SalesTicketRequest(showId)).catch {
}.firstOrNull()?.let { tickets ->
_uiState.update { it.copy(tickets = tickets) }
}
repository.getSalesTickets(SalesTicketRequest(showId))
.firstOrNull()?.let { tickets ->
_uiState.update { it.copy(tickets = tickets) }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.nexters.boolti.presentation.screen.ticketing

import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.animateContentSize
Expand Down Expand Up @@ -92,6 +91,9 @@ import com.nexters.boolti.presentation.theme.marginHorizontal
import com.nexters.boolti.presentation.theme.point2
import com.nexters.boolti.presentation.util.PhoneNumberVisualTransformation
import com.nexters.boolti.tosspayments.TossPaymentWidgetActivity
import com.nexters.boolti.tosspayments.TossPaymentWidgetActivity.Companion.RESULT_FAIL
import com.nexters.boolti.tosspayments.TossPaymentWidgetActivity.Companion.RESULT_SOLD_OUT
import com.nexters.boolti.tosspayments.TossPaymentWidgetActivity.Companion.RESULT_SUCCESS
import java.time.LocalDateTime

@Composable
Expand All @@ -106,15 +108,23 @@ fun TicketingScreen(
val snackbarHostState = remember { SnackbarHostState() }
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var showConfirmDialog by remember { mutableStateOf(false) }
var showPaymentFailureDialog by remember { mutableStateOf(false) }
var showTicketSoldOutDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val uriHandler = LocalUriHandler.current

val paymentLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data ?: return@rememberLauncherForActivityResult
val reservationId = intent.getStringExtra("reservationId") ?: return@rememberLauncherForActivityResult
onReserved(reservationId, viewModel.showId)
when (result.resultCode) {
RESULT_SUCCESS -> {
val intent = result.data ?: return@rememberLauncherForActivityResult
val reservationId =
intent.getStringExtra("reservationId") ?: return@rememberLauncherForActivityResult
onReserved(reservationId, viewModel.showId)
}

RESULT_SOLD_OUT -> showTicketSoldOutDialog = true
RESULT_FAIL -> showPaymentFailureDialog = true
}
}

Expand Down Expand Up @@ -299,6 +309,17 @@ fun TicketingScreen(
onDismiss = { showConfirmDialog = false },
)
}
if (showPaymentFailureDialog) {
PaymentFailureDialog {
showPaymentFailureDialog = false
}
}
if (showTicketSoldOutDialog) {
PaymentFailureDialog {
showTicketSoldOutDialog = false
onBackClicked()
}
}
}
}

Expand Down
7 changes: 5 additions & 2 deletions presentation/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
<string name="ticketing_contact_placeholder">숫자만 입력해 주세요</string>
<string name="ticketing_invite_code_placeholder">초청 코드를 입력해주세요</string>
<string name="ticketing_invite_code_use_button">사용하기</string>
<string name="ticketing_invite_code_success">사용되었습니다</string>
<string name="ticketing_invite_code_success">사용되었어요</string>
<string name="ticketing_invite_code_duplicated">이미 사용된 초청 코드입니다</string>
<string name="ticketing_invite_code_invalid">올바른 초청 코드를 입력해 주세요</string>
<string name="ticketing_invite_code_empty">초청 코드를 입력해 주세요</string>
Expand All @@ -154,9 +154,10 @@
<string name="ticketing_payment_message">지금은 계좌 이체로만 결제할 수 있어요</string>
<string name="ticketing_payment_button_label">%,d원 결제하기</string>
<string name="ticketing_payment_button_label_short">결제하기</string>
<string name="ticketing_confirm_dialog_title">결제 정보를 확인해주세요</string>
<string name="ticketing_confirm_dialog_title">결제 정보를 확인해 주세요</string>
<string name="ticketing_address_copied_message">공연장 주소가 복사되었어요</string>
<string name="ticketing_host_format">%s (%s)</string>
<string name="ticketing_again">다시 예매하기</string>
<string name="order_agreement_label">주문내용 확인 및 결제 동의</string>
<string name="order_agreement_privacy_collection">[필수] 개인정보 수집・이용 동의</string>
<string name="order_agreement_privacy_offer">[필수] 개인정보 제 3자 정보 제공 동의</string>
Expand All @@ -182,6 +183,8 @@
<string name="account_transfer_due_date">입금 마감일</string>
<string name="invite_ticket_complete_title">결제를 완료했어요</string>
<string name="invite_ticket_complete_description">예매자 정보 확인 후 티켓이 발권됩니다.</string>
<string name="payment_failed">결제에 실패했어요</string>
<string name="payment_failed_description">예매 진행 중 오류가 발생하였습니다.\n다시 시도해 주세요</string>

<!-- 마이 -->
<string name="my_login">불티 로그인 하러가기</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ sealed interface PaymentEvent {
val orderId: String,
val reservationId: String,
) : PaymentEvent

data object TicketSoldOut : PaymentEvent
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.nexters.boolti.tosspayments

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
Expand Down Expand Up @@ -32,15 +31,6 @@ class TossPaymentWidgetActivity : AppCompatActivity() {
private lateinit var binding: ActivityTossPaymentWidgetBinding
private val viewModel: TossPaymentsWidgetViewModel by viewModels()

private val paymentFailureDialog by lazy {
BTDialog().apply {
title = this@TossPaymentWidgetActivity.getString(R.string.payment_failed_title)
buttonLabel = this@TossPaymentWidgetActivity.getString(R.string.payment_failure_cta)
isCancelable = false
listener = BTDialogListener { finish() }
}
}

private val paymentEventListener
get() = object : PaymentMethodEventListener() {
override fun onCustomRequested(paymentMethodKey: String) {
Expand Down Expand Up @@ -145,7 +135,12 @@ class TossPaymentWidgetActivity : AppCompatActivity() {
putExtra("orderId", event.orderId)
putExtra("reservationId", event.reservationId)
}
setResult(Activity.RESULT_OK, intent)
setResult(RESULT_SUCCESS, intent)
finish()
}

is PaymentEvent.TicketSoldOut -> {
setResult(RESULT_SOLD_OUT)
finish()
}
}
Expand Down Expand Up @@ -221,17 +216,20 @@ class TossPaymentWidgetActivity : AppCompatActivity() {
TAG,
"handlePaymentFailResult. error: ${fail.errorCode}, message: ${fail.errorMessage}, orderId: ${fail.orderId}"
)
paymentFailureDialog.apply {
message = fail.errorMessage
show(supportFragmentManager, fail.errorCode)
}
if (fail.errorCode == "PAY_PROCESS_CANCELED") return

setResult(RESULT_FAIL)
finish()
}

private fun toast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

companion object {
const val RESULT_SUCCESS = 200
const val RESULT_FAIL = 400
const val RESULT_SOLD_OUT = 401
private const val TAG = "PaymentWidgetActivity"
private const val EXTRA_KEY_AMOUNT = "extraKeyAmount"
private const val EXTRA_KEY_CLIENT_KEY = "extraKeyClientKey"
Expand Down
Loading

0 comments on commit fb4455c

Please sign in to comment.