Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/boolti 347 웹뷰 브릿지 연결 (공연 등록) #358

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.nexters.boolti.data.datasource

import javax.inject.Inject

internal class AuthTokenDataSource @Inject constructor(
private val authDataSource: AuthDataSource,
private val tokenDataSource: TokenDataSource,
) {
suspend fun getNewAccessToken(): String? {
val response = authDataSource.refresh()
val newToken = response.getOrNull()
return newToken?.let {
tokenDataSource.saveTokens(
accessToken = it.accessToken,
refreshToken = it.refreshToken,
)
it.accessToken
} ?: run {
authDataSource.logout()
null
}
}
}
11 changes: 11 additions & 0 deletions data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.nexters.boolti.data.di
import android.content.Context
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.nexters.boolti.data.datasource.AuthDataSource
import com.nexters.boolti.data.datasource.AuthTokenDataSource
import com.nexters.boolti.data.datasource.PolicyDataSource
import com.nexters.boolti.data.datasource.RemoteConfigDataSource
import com.nexters.boolti.data.datasource.TokenDataSource
Expand Down Expand Up @@ -33,6 +34,16 @@ internal object DataSourceModule {
@Provides
fun provideTokenDataSource(@ApplicationContext context: Context): TokenDataSource = TokenDataSource(context)

@Singleton
@Provides
fun provideAuthTokenDataSource(
authDataSource: AuthDataSource,
tokenDataSource: TokenDataSource,
): AuthTokenDataSource = AuthTokenDataSource(
authDataSource = authDataSource,
tokenDataSource = tokenDataSource,
)

@Singleton
@Provides
fun providePolicyDataSource(@ApplicationContext context: Context): PolicyDataSource = PolicyDataSource(context)
Expand Down
27 changes: 17 additions & 10 deletions data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.nexters.boolti.data.di

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.nexters.boolti.data.BuildConfig
import com.nexters.boolti.data.datasource.AuthDataSource
import com.nexters.boolti.data.datasource.AuthTokenDataSource
import com.nexters.boolti.data.datasource.TokenDataSource
import com.nexters.boolti.data.network.AuthAuthenticator
import com.nexters.boolti.data.network.AuthInterceptor
Expand Down Expand Up @@ -114,7 +114,8 @@ internal object NetworkModule {

@Singleton
@Provides
fun provideTicketingService(@Named("auth") retrofit: Retrofit): TicketingService = retrofit.create()
fun provideTicketingService(@Named("auth") retrofit: Retrofit): TicketingService =
retrofit.create()

@Singleton
@Provides
Expand All @@ -126,28 +127,34 @@ internal object NetworkModule {

@Singleton
@Provides
fun provideReservationService(@Named("auth") retrofit: Retrofit): ReservationService = retrofit.create()
fun provideReservationService(@Named("auth") retrofit: Retrofit): ReservationService =
retrofit.create()

@Singleton
@Provides
fun provideHostService(@Named("auth") retrofit: Retrofit): HostService = retrofit.create()

@Singleton
@Provides
fun provideAuthFileService(@Named("auth") retrofit: Retrofit): AuthFileService = retrofit.create()
fun provideAuthFileService(@Named("auth") retrofit: Retrofit): AuthFileService =
retrofit.create()

@Singleton
@Provides
fun provideFileService(@Named("non-auth") retrofit: Retrofit): FileService = retrofit.create()

@Singleton
@Provides
fun provideMemberService(@Named("non-auth") retrofit: Retrofit): MemberService = retrofit.create()
fun provideMemberService(@Named("non-auth") retrofit: Retrofit): MemberService =
retrofit.create()

@Singleton
@Provides
@Named("auth")
fun provideAuthOkHttpClient(interceptor: AuthInterceptor, authenticator: AuthAuthenticator): OkHttpClient {
fun provideAuthOkHttpClient(
interceptor: AuthInterceptor,
authenticator: AuthAuthenticator
): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
Expand Down Expand Up @@ -202,12 +209,12 @@ internal object NetworkModule {

@Singleton
@Provides
fun provideAuthInterceptor(tokenDataSource: TokenDataSource): AuthInterceptor = AuthInterceptor(tokenDataSource)
fun provideAuthInterceptor(tokenDataSource: TokenDataSource): AuthInterceptor =
AuthInterceptor(tokenDataSource)

@Singleton
@Provides
fun provideAuthenticator(
tokenDataSource: TokenDataSource,
authDataSource: AuthDataSource,
): AuthAuthenticator = AuthAuthenticator(tokenDataSource, authDataSource)
authTokenDataSource: AuthTokenDataSource
): AuthAuthenticator = AuthAuthenticator(authTokenDataSource)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.nexters.boolti.data.network

import com.nexters.boolti.data.datasource.AuthDataSource
import com.nexters.boolti.data.datasource.TokenDataSource
import com.nexters.boolti.data.datasource.AuthTokenDataSource
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
Expand All @@ -10,27 +9,11 @@ import okhttp3.Route
import javax.inject.Inject

internal class AuthAuthenticator @Inject constructor(
private val tokenDataSource: TokenDataSource,
private val authDataSource: AuthDataSource,
private val authTokenDataSource: AuthTokenDataSource,
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val accessToken = runBlocking { getNewAccessToken() } ?: return null
val accessToken = runBlocking { authTokenDataSource.getNewAccessToken() } ?: return null

return response.request.newBuilder().header("Authorization", "Bearer $accessToken").build()
}

private suspend fun getNewAccessToken(): String? {
val response = authDataSource.refresh()
val newToken = response.getOrNull()
return newToken?.let {
tokenDataSource.saveTokens(
accessToken = it.accessToken,
refreshToken = it.refreshToken,
)
it.accessToken
} ?: run {
authDataSource.logout()
null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.nexters.boolti.data.repository

import com.nexters.boolti.data.datasource.AuthDataSource
import com.nexters.boolti.data.datasource.AuthTokenDataSource
import com.nexters.boolti.data.datasource.DeviceTokenDataSource
import com.nexters.boolti.data.datasource.SignUpDataSource
import com.nexters.boolti.data.datasource.TokenDataSource
import com.nexters.boolti.data.datasource.UserDataSource
import com.nexters.boolti.data.network.response.LoginResponse
import com.nexters.boolti.domain.model.AccessToken
import com.nexters.boolti.domain.model.LoginUserState
import com.nexters.boolti.domain.model.TokenPair
import com.nexters.boolti.domain.model.TokenPairs
import com.nexters.boolti.domain.model.User
import com.nexters.boolti.domain.repository.AuthRepository
import com.nexters.boolti.domain.request.EditProfileRequest
Expand All @@ -23,6 +25,7 @@ import javax.inject.Inject
internal class AuthRepositoryImpl @Inject constructor(
private val authDataSource: AuthDataSource,
private val tokenDataSource: TokenDataSource,
private val authTokenDataSource: AuthTokenDataSource,
private val signUpDataSource: SignUpDataSource,
private val userDateSource: UserDataSource,
private val deviceTokenDataSource: DeviceTokenDataSource,
Expand Down Expand Up @@ -72,7 +75,13 @@ internal class AuthRepositoryImpl @Inject constructor(
.onSuccess { authDataSource.updateUser(it) }
.mapCatching {}

override fun getTokens(): Flow<TokenPair> = authDataSource.getTokens().map {
TokenPair(it.first, it.second)
override fun getTokens(): Flow<TokenPairs> = authDataSource.getTokens().map {
TokenPairs(it.first, it.second)
}

override fun refreshToken(): Flow<AccessToken> = flow {
authTokenDataSource.getNewAccessToken()?.let {
emit(AccessToken(it))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.nexters.boolti.domain.model

data class TokenPair(
data class TokenPairs(
val accessToken: String,
val refreshToken: String,
) {
val isLoggedIn: Boolean = accessToken.isNotBlank() && refreshToken.isNotBlank()
}

@JvmInline
value class AccessToken(val token: String)

@JvmInline
value class RefreshToken(val token: String)
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.nexters.boolti.domain.repository

import com.nexters.boolti.domain.model.AccessToken
import com.nexters.boolti.domain.model.LoginUserState
import com.nexters.boolti.domain.model.TokenPair
import com.nexters.boolti.domain.model.TokenPairs
import com.nexters.boolti.domain.model.User
import com.nexters.boolti.domain.request.EditProfileRequest
import com.nexters.boolti.domain.request.LoginRequest
Expand Down Expand Up @@ -30,5 +31,7 @@ interface AuthRepository {

suspend fun editProfile(editProfileRequest: EditProfileRequest): Result<Unit>

fun getTokens(): Flow<TokenPair>
fun getTokens(): Flow<TokenPairs>
fun refreshToken(): Flow<AccessToken>
}

3 changes: 3 additions & 0 deletions presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.serialization)
id("kotlin-kapt")
id("kotlin-parcelize")
}
Expand Down Expand Up @@ -82,6 +83,8 @@ dependencies {
implementation(libs.zoomable)
kapt(libs.hilt.compiler)

implementation(libs.kotlinx.serialization.json)

implementation(libs.lottie)
implementation(libs.bundles.coil)
api(libs.kakao.login)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import com.nexters.boolti.presentation.util.bridge.BridgeDto
import com.nexters.boolti.presentation.util.bridge.BridgeManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import timber.log.Timber

class BtWebView @JvmOverloads constructor(
context: Context,
Expand Down Expand Up @@ -53,6 +60,38 @@ class BtWebView @JvmOverloads constructor(
onProgressChanged = { _progress.value = it },
)
}

suspend fun setBridgeManager(bridgeManager: BridgeManager) {
addJavascriptInterface(
object {
@JavascriptInterface
fun postMessage(message: String) {
Timber.tag("webview_bridge").d("(WEB -> APP) $message 수신")
try {
// JSON 메시지 파싱
val dto = Json.decodeFromString(BridgeDto.serializer(), message)
bridgeManager.handleIncomingData(dto)
} catch (e: SerializationException) {
Timber.tag("webview_bridge")
.e(e, "(WEB -> APP) 유효하지 않은 JSON 포맷: $message")
} catch (e: IllegalArgumentException) {
Timber.tag("webview_bridge")
.e(e, "(WEB -> APP) BridgeDto 타입으로 파싱 실패: $message")
} catch (e: Exception) {
Timber.tag("webview_bridge")
.e(e, "(WEB -> APP) 알 수 없는 에러")
e.printStackTrace() // 에러 처리
}
}
},
bridgeManager.bridgeName,
)
bridgeManager.dataToSendWeb.collect {
evaluateJavascript(it) { result ->
Timber.tag("webview_bridge").d("(APP -> WEB)\n\t$it\n전송 결과:\n\t$result")
}
}
}
}

class BtWebViewClient : WebViewClient()
Expand All @@ -79,4 +118,10 @@ class BtWebChromeClient(

return true
}

override fun onConsoleMessage(message: ConsoleMessage?): Boolean {
Timber.tag("webview_console Message bridge")
.d("${message?.message()} -- From line ${message?.lineNumber()} of ${message?.sourceId()}")
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName:
addShowRegistration(
modifier = modifier,
popBackStack = navController::popBackStack,
navigateTo = navController::navigateTo,
navigateToHome = navController::navigateToHome,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.nexters.boolti.presentation.screen.my

import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -78,10 +77,7 @@ fun MyScreen(
onClickHeaderButton = if (user != null) navigateToProfile else requireLogin,
onClickAccountSetting = if (user != null) onClickAccountSetting else requireLogin,
onClickReservations = if (user != null) navigateToReservations else requireLogin,
onClickRegisterShow = {
uriHandler.openUri(url)
Toast.makeText(context, "공연 등록을 위해 웹으로 이동합니다", Toast.LENGTH_LONG).show()
},// navigateToShowRegistration, // TODO 추후 인앱 공연 등록 반영 시 주석 해제
onClickRegisterShow = if (user != null) navigateToShowRegistration else requireLogin,
onClickQrScan = if (user != null) onClickQrScan else requireLogin,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import com.nexters.boolti.presentation.screen.MainDestination
fun NavGraphBuilder.addShowRegistration(
modifier: Modifier = Modifier,
popBackStack: () -> Unit,
navigateTo: (route: String) -> Unit,
navigateToHome: () -> Unit,
) {
composable(
route = MainDestination.ShowRegistration.route,
) {
ShowRegistrationScreen(
modifier = modifier,
onClickBack = popBackStack,
navigateTo = navigateTo,
navigateToHome = navigateToHome,
)
}
}
Loading