diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt new file mode 100644 index 00000000..0f7c0554 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt @@ -0,0 +1,160 @@ +package kr.co.vividnext.sodalive.v2.access + +import android.app.Activity +import android.content.Intent +import androidx.fragment.app.Fragment +import com.google.gson.Gson +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.mypage.MyPageViewModel +import kr.co.vividnext.sodalive.mypage.auth.Auth +import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest +import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse +import kr.co.vividnext.sodalive.settings.ContentSettingsActivity +import kr.co.vividnext.sodalive.splash.SplashActivity +import kr.co.vividnext.sodalive.user.login.LoginActivity +import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog +import org.koin.android.ext.android.get + +class V2AccessGuard( + private val policy: AccessPolicy = AccessPolicy(), + private val stateProvider: AccessStateProvider = SharedPreferenceAccessStateProvider() +) { + fun ensureAccess( + activity: Activity, + requirement: AccessRequirement, + onAllowed: () -> Unit = {} + ): Boolean { + return handleDecision( + activity = activity, + requirement = requirement, + startLogin = { startLoginActivity(activity) }, + onAllowed = onAllowed + ) + } + + fun ensureAccess( + fragment: Fragment, + requirement: AccessRequirement, + onAllowed: () -> Unit = {} + ): Boolean { + val activity = fragment.requireActivity() + return handleDecision( + activity = activity, + requirement = requirement, + startLogin = { startLoginActivity(activity) }, + onAllowed = onAllowed + ) + } + + fun isAllowed(requirement: AccessRequirement): Boolean { + return policy.decide(requirement, stateProvider.current()) == AccessDecision.Allowed + } + + private fun handleDecision( + activity: Activity, + requirement: AccessRequirement, + startLogin: () -> Unit, + onAllowed: () -> Unit + ): Boolean { + return executeAccessDecision( + decision = policy.decide(requirement, stateProvider.current()), + onAllowed = onAllowed, + onLoginRequired = startLogin, + onAdultVerificationRequired = { showAuthDialog(activity) }, + onAdultContentSettingRequired = { startContentSettings(activity) } + ) + } + + private fun startLoginActivity(activity: Activity) { + activity.startActivity( + Intent(activity, LoginActivity::class.java).apply { + putExtra(Constants.EXTRA_DATA, activity.intent.extras) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + ) + } + + private fun showAuthDialog(activity: Activity) { + V2ModalDialog( + activity = activity, + layoutInflater = activity.layoutInflater, + title = activity.getString(R.string.auth_title), + desc = activity.getString(R.string.auth_desc_live), + confirmButtonTitle = activity.getString(R.string.auth_go), + confirmButtonClick = { startAuthFlow(activity) }, + cancelButtonTitle = activity.getString(R.string.cancel), + cancelButtonClick = {} + ).show(activity.resources.displayMetrics.widthPixels) + } + + private fun startAuthFlow(activity: Activity) { + val myPageViewModel: MyPageViewModel = activity.get() + Auth.auth(activity, activity) { json -> + val bootpayResponse = Gson().fromJson(json, BootpayResponse::class.java) + val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId) + activity.runOnUiThread { + if (activity.isFinishing || activity.isDestroyed) return@runOnUiThread + myPageViewModel.authVerify(request) { + if (activity.isFinishing || activity.isDestroyed) return@authVerify + activity.startActivity( + Intent(activity, SplashActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + activity.finish() + } + } + } + } + + private fun startContentSettings(activity: Activity) { + activity.startActivity( + Intent(activity, ContentSettingsActivity::class.java).apply { + putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true) + } + ) + } +} + +internal fun executeAccessDecision( + decision: AccessDecision, + onAllowed: () -> Unit, + onLoginRequired: () -> Unit, + onAdultVerificationRequired: () -> Unit, + onAdultContentSettingRequired: () -> Unit +): Boolean { + return when (decision) { + AccessDecision.Allowed -> { + onAllowed() + true + } + AccessDecision.LoginRequired -> { + onLoginRequired() + false + } + AccessDecision.AdultVerificationRequired -> { + onAdultVerificationRequired() + false + } + AccessDecision.AdultContentSettingRequired -> { + onAdultContentSettingRequired() + false + } + } +} + +fun Activity.ensureV2Access( + requirement: AccessRequirement, + onAllowed: () -> Unit = {} +): Boolean = V2AccessGuard().ensureAccess(this, requirement, onAllowed) + +fun Fragment.ensureV2Access( + requirement: AccessRequirement, + onAllowed: () -> Unit = {} +): Boolean = V2AccessGuard().ensureAccess(this, requirement, onAllowed) + +fun isV2AccessAllowed(requirement: AccessRequirement): Boolean { + return V2AccessGuard().isAllowed(requirement) +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuardTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuardTest.kt new file mode 100644 index 00000000..49c380b9 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuardTest.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.v2.access + +import org.junit.Assert.assertEquals +import org.junit.Test + +class V2AccessGuardTest { + + @Test + fun `각 Access 결과는 대응하는 action만 정확히 한 번 실행한다`() { + val cases = listOf( + AccessDecision.Allowed to "allowed", + AccessDecision.LoginRequired to "login", + AccessDecision.AdultVerificationRequired to "adultVerification", + AccessDecision.AdultContentSettingRequired to "adultContentSetting" + ) + + cases.forEach { (decision, expectedAction) -> + val executedActions = mutableListOf() + + val isAllowed = executeAccessDecision( + decision = decision, + onAllowed = { executedActions += "allowed" }, + onLoginRequired = { executedActions += "login" }, + onAdultVerificationRequired = { executedActions += "adultVerification" }, + onAdultContentSettingRequired = { executedActions += "adultContentSetting" } + ) + + assertEquals(decision == AccessDecision.Allowed, isAllowed) + assertEquals(listOf(expectedAction), executedActions) + } + } +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuardSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuardSourceTest.kt index fee1a593..fb0cf2c0 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuardSourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuardSourceTest.kt @@ -8,27 +8,50 @@ import java.io.File class MainV2LoginGuardSourceTest { @Test - fun `MainV2 로그인 가드는 v2 Activity 로그인 진입점과 성인 콘텐츠 설정을 사용한다`() { - val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2LoginGuard.kt").readText() + fun `V2AccessGuard는 기존 로그인 본인인증 성인 콘텐츠 설정 이동 계약을 유지한다`() { + val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/V2AccessGuard.kt").readText() + val policy = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessPolicy.kt").readText() + val state = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/access/AccessState.kt").readText() + val mainV2Activity = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt" + ).readText() + val handleDecisionSource = source.substring( + source.indexOf("private fun handleDecision("), + source.indexOf("private fun startLoginActivity") + ) - assertTrue(source.contains("fun Fragment.ensureMainV2NavigationAllowed(")) - assertTrue(source.contains("requiresAdultContentAccess: Boolean = false")) - assertTrue(source.contains("SharedPreferenceManager.token.isBlank()")) - assertTrue(source.contains("(activity as? MainV2Activity)?.showLoginActivity()")) - assertTrue(source.contains("SharedPreferenceManager.countryCode.ifBlank { \"KR\" }")) - assertTrue(source.contains("SharedPreferenceManager.isAuth")) + assertTrue(source.contains("fun Activity.ensureV2Access(")) + assertTrue(source.contains("fun Fragment.ensureV2Access(")) + assertTrue(source.contains("fun isV2AccessAllowed(requirement: AccessRequirement): Boolean")) + assertTrue(source.contains("fun isAllowed(requirement: AccessRequirement): Boolean")) + assertTrue(source.contains("AccessDecision.Allowed")) + assertTrue(source.contains("onAllowed()")) + assertTrue(handleDecisionSource.contains("onLoginRequired = startLogin")) + assertTrue(handleDecisionSource.contains("onAdultVerificationRequired = { showAuthDialog(activity) }")) + assertTrue(handleDecisionSource.contains("onAdultContentSettingRequired = { startContentSettings(activity) }")) + assertTrue(source.contains("Intent(activity, LoginActivity::class.java)")) + assertTrue(source.contains("putExtra(Constants.EXTRA_DATA, activity.intent.extras)")) + assertTrue(source.contains("Intent.FLAG_ACTIVITY_CLEAR_TOP")) + assertTrue(source.contains("Intent.FLAG_ACTIVITY_SINGLE_TOP")) assertTrue(source.contains("V2ModalDialog(")) assertFalse(source.contains("SodaDialog(")) - assertTrue(source.contains("Auth.auth(requireActivity(), requireContext())")) + assertTrue(source.contains("Auth.auth(activity, activity)")) assertTrue(source.contains("Gson().fromJson(json, BootpayResponse::class.java)")) assertTrue(source.contains("AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)")) assertTrue(source.contains("myPageViewModel.authVerify(request)")) + assertTrue(source.contains("if (activity.isFinishing || activity.isDestroyed)")) assertTrue(source.contains("SplashActivity::class.java")) - assertTrue(source.contains("SharedPreferenceManager.isAdultContentVisible")) + assertTrue(source.contains("Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK")) assertTrue(source.contains("ContentSettingsActivity::class.java")) assertTrue(source.contains("putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true)")) - assertFalse(source.contains("Auth.auth(requireActivity(), requireContext()) {}")) - assertFalse(source.contains("kr.co.vividnext.sodalive.main.MainActivity")) + assertFalse(source.contains("Auth.auth(activity, activity) {}")) + assertTrue(policy.contains("state.token.isNotBlank() && state.token.length > LOGIN_TOKEN_MIN_LENGTH")) + assertTrue(policy.contains("state.countryCode.ifBlank { \"KR\" } == \"KR\"")) + assertTrue(state.contains("SharedPreferenceManager.token")) + assertTrue(state.contains("SharedPreferenceManager.isAuth")) + assertTrue(state.contains("SharedPreferenceManager.isAdultContentVisible")) + assertTrue(mainV2Activity.contains("fun showLoginActivity()")) + assertTrue(mainV2Activity.contains("ensureV2Access(AccessRequirement.Login)")) } private fun projectFile(relativePath: String): File {