From 7dc2eabeef4455e6b4a43eaf45f0a0b8e9e7e853 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Wed, 11 Dec 2024 16:15:37 +0900 Subject: [PATCH 01/13] =?UTF-8?q?Boolti-347=20feat:=20=EC=9B=B9=EB=B7=B0?= =?UTF-8?q?=20=EB=B8=8C=EB=A6=BF=EC=A7=80=20=EC=97=B0=EA=B2=B0.=20(todo:?= =?UTF-8?q?=20app=20to=20web)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/datasource/AuthTokenDataSource.kt | 23 ++++ .../boolti/data/di/DataSourceModule.kt | 11 ++ .../nexters/boolti/data/di/NetworkModule.kt | 27 +++-- .../boolti/data/network/AuthAuthenticator.kt | 23 +--- .../data/repository/AuthRepositoryImpl.kt | 15 ++- .../model/{TokenPair.kt => TokenPairs.kt} | 8 +- .../domain/repository/AuthRepository.kt | 7 +- presentation/build.gradle.kts | 3 + .../presentation/component/BtWebView.kt | 113 ++++++++++++++++++ .../ShowRegistrationScreen.kt | 37 +++++- .../ShowRegistrationViewModel.kt | 6 +- 11 files changed, 233 insertions(+), 40 deletions(-) create mode 100644 data/src/main/java/com/nexters/boolti/data/datasource/AuthTokenDataSource.kt rename domain/src/main/java/com/nexters/boolti/domain/model/{TokenPair.kt => TokenPairs.kt} (58%) diff --git a/data/src/main/java/com/nexters/boolti/data/datasource/AuthTokenDataSource.kt b/data/src/main/java/com/nexters/boolti/data/datasource/AuthTokenDataSource.kt new file mode 100644 index 00000000..172a8a36 --- /dev/null +++ b/data/src/main/java/com/nexters/boolti/data/datasource/AuthTokenDataSource.kt @@ -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 + } + } +} diff --git a/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt b/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt index fe59d845..ff04d2e7 100644 --- a/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt +++ b/data/src/main/java/com/nexters/boolti/data/di/DataSourceModule.kt @@ -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 @@ -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) diff --git a/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt b/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt index 16c70e6b..b6241c0b 100644 --- a/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt +++ b/data/src/main/java/com/nexters/boolti/data/di/NetworkModule.kt @@ -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 @@ -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 @@ -126,7 +127,8 @@ 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 @@ -134,7 +136,8 @@ internal object NetworkModule { @Singleton @Provides - fun provideAuthFileService(@Named("auth") retrofit: Retrofit): AuthFileService = retrofit.create() + fun provideAuthFileService(@Named("auth") retrofit: Retrofit): AuthFileService = + retrofit.create() @Singleton @Provides @@ -142,12 +145,16 @@ internal object NetworkModule { @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 @@ -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) } diff --git a/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt b/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt index 8ae8e232..55ac1fd5 100644 --- a/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt +++ b/data/src/main/java/com/nexters/boolti/data/network/AuthAuthenticator.kt @@ -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 @@ -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 - } - } } diff --git a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt index 9835a6d9..1ae60fd7 100644 --- a/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/nexters/boolti/data/repository/AuthRepositoryImpl.kt @@ -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 @@ -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, @@ -72,7 +75,13 @@ internal class AuthRepositoryImpl @Inject constructor( .onSuccess { authDataSource.updateUser(it) } .mapCatching {} - override fun getTokens(): Flow = authDataSource.getTokens().map { - TokenPair(it.first, it.second) + override fun getTokens(): Flow = authDataSource.getTokens().map { + TokenPairs(it.first, it.second) + } + + override fun refreshToken(): Flow = flow { + authTokenDataSource.getNewAccessToken()?.let { + emit(AccessToken(it)) + } } } diff --git a/domain/src/main/java/com/nexters/boolti/domain/model/TokenPair.kt b/domain/src/main/java/com/nexters/boolti/domain/model/TokenPairs.kt similarity index 58% rename from domain/src/main/java/com/nexters/boolti/domain/model/TokenPair.kt rename to domain/src/main/java/com/nexters/boolti/domain/model/TokenPairs.kt index ab04154a..e79184ff 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/model/TokenPair.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/model/TokenPairs.kt @@ -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) diff --git a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt index 9ac3a335..a2b6e65f 100644 --- a/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt +++ b/domain/src/main/java/com/nexters/boolti/domain/repository/AuthRepository.kt @@ -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 @@ -30,5 +31,7 @@ interface AuthRepository { suspend fun editProfile(editProfileRequest: EditProfileRequest): Result - fun getTokens(): Flow + fun getTokens(): Flow + fun refreshToken(): Flow } + diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index d00668f5..3598d9e7 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -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") } @@ -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) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt index 41326bfa..8a8277fa 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt @@ -4,12 +4,22 @@ import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.util.AttributeSet +import android.webkit.JavascriptInterface import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import timber.log.Timber +import java.util.UUID class BtWebView @JvmOverloads constructor( context: Context, @@ -21,12 +31,27 @@ class BtWebView @JvmOverloads constructor( private val _progress = MutableStateFlow(0) val progress = _progress.asStateFlow() + private val _bridgeEvent = MutableSharedFlow( + extraBufferCapacity = 5, + ) + val bridgeEvent = _bridgeEvent.asSharedFlow() + + private val json = Json { + isLenient = true + prettyPrint = true + ignoreUnknownKeys = true + } + init { isFocusable = true isFocusableInTouchMode = true setupSettings() setupWebViewClient() + setupBridge() + /*evaluate("밍슈 하이~") { + Timber.tag("MANGBAAM-BtWebView()").d("웹 콜백: $it") + }*/ } @SuppressLint("SetJavaScriptEnabled") @@ -43,6 +68,25 @@ class BtWebView @JvmOverloads constructor( webViewClient = BtWebViewClient() } + private fun setupBridge() { + addJavascriptInterface( + NativeBridge { response -> + val data = response.toBridgeData() + _bridgeEvent.tryEmit(data).also { + Timber.tag("MANGBAAM-BtWebView(setupBridge)").d("성공? $it") + } + }, + "boolti", + ) + } + + fun evaluate(data: BridgeRequest, result: (String) -> Unit = {}) { + val dataAsString = json.encodeToString(data) + evaluateJavascript("javascript:postMessage('$dataAsString')") { + result(it) + } + } + fun setWebChromeClient( launchActivity: () -> Unit, setFilePathCallback: (ValueCallback>) -> Unit, @@ -80,3 +124,72 @@ class BtWebChromeClient( return true } } + +class NativeBridge(private val callback: (BridgeResponse) -> Unit = {}) { + @JavascriptInterface + fun postMessage(jsonString: String) { + runCatching { json.decodeFromString(jsonString) } + .onSuccess(callback) + .onFailure { it.printStackTrace() } // TODO 에러 처리 + } + + companion object { + private val json = Json { + isLenient = true + prettyPrint = true + ignoreUnknownKeys = true + } + } +} + +enum class Command { + NAVIGATE_TO_SHOW_DETAIL, + NAVIGATE_BACK, + REQUEST_TOKEN, + UNKNOWN; + + companion object { + fun fromString(value: String): Command = + Command.entries.find { it.name == value.trim().uppercase() } ?: UNKNOWN + } +} + +sealed interface BridgeData { + data class ShowDetail(val id: String) : BridgeData + data object NavigateBack : BridgeData + data object RequestToken : BridgeData + data object Unknown : BridgeData +} + +@Serializable +data class BridgeResponse( + val id: String = UUID.randomUUID().toString(), + val timestamp: String = System.currentTimeMillis().toString(), + val command: String, + val data: JsonElement? = null, +) { + fun toBridgeData(): BridgeData = when (Command.fromString(command)) { + Command.NAVIGATE_TO_SHOW_DETAIL -> { + val showId = when (data) { + is JsonObject -> requireNotNull(data["showId"]?.toString()) { + Timber.tag("MANGBAAM-BridgeResponse(toBridgeData)").d("showId is undefined") + } + + else -> throw IllegalArgumentException("data is not JsonObject") + } + BridgeData.ShowDetail(showId) + } + + Command.NAVIGATE_BACK -> BridgeData.NavigateBack + Command.REQUEST_TOKEN -> BridgeData.RequestToken + Command.UNKNOWN -> BridgeData.Unknown + } +} + +@Serializable +data class BridgeRequest( + val command: String, + val data: String? = null, + val id: String = UUID.randomUUID().toString(), + val timestamp: String = System.currentTimeMillis().toString(), +) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt index aab032df..626afaa5 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt @@ -5,7 +5,6 @@ import android.net.Uri import android.webkit.CookieManager import android.webkit.ValueCallback import android.webkit.WebStorage -import android.webkit.WebView import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -36,9 +35,12 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.nexters.boolti.presentation.BuildConfig import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog +import com.nexters.boolti.presentation.component.BridgeData +import com.nexters.boolti.presentation.component.BridgeRequest import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.BtCircularProgressIndicator import com.nexters.boolti.presentation.component.BtWebView +import com.nexters.boolti.presentation.component.Command import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -64,7 +66,7 @@ fun ShowRegistrationScreen( var showExitDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - var webView: WebView? by remember { mutableStateOf(null) } + var webView: BtWebView? by remember { mutableStateOf(null) } var webviewProgress by remember { mutableIntStateOf(0) } val loading by remember { derivedStateOf { webviewProgress < 100 } } @@ -81,6 +83,14 @@ fun ShowRegistrationScreen( setCookie(url, "x-refresh-token=${tokens.refreshToken}") flush() } + /*launch { + webView?.evaluate( + BridgeRequest( + command = Command.REQUEST_TOKEN.name, + data = tokens.accessToken, + ) + ) + }*/ } } @@ -114,7 +124,28 @@ fun ShowRegistrationScreen( Timber.d("내가 만든 쿠키 : ${CookieManager.getInstance().getCookie(url)}") scope.launch { - progress.collect { webviewProgress = it } + launch { + progress.collect { webviewProgress = it } + } + launch { + bridgeEvent.collect { data -> + Timber.tag("MANGBAAM-(ShowRegistrationScreen)").d("$data") + when (data) { + is BridgeData.RequestToken -> { + evaluate( + BridgeRequest( + command = Command.REQUEST_TOKEN.name, + data = "hello", + ) + ) + } + + else -> { + Timber.tag("MANGBAAM-(ShowRegistrationScreen)").d("안 탔나?") + } + } + } + } } }.also { webView = it } }, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt index d3999d3d..8861822e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt @@ -13,4 +13,8 @@ class ShowRegistrationViewModel @Inject constructor( ) : ViewModel() { val tokens = authRepository.getTokens() .stateInUi(viewModelScope, null) -} \ No newline at end of file + + fun refreshToken() { + authRepository.refreshToken() + } +} From 1a9b135cf175f0e4fab5a95c5b7672436c49f6f2 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Thu, 12 Dec 2024 03:55:39 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feature/Boolti-347=20feat:=20=EB=B8=8C?= =?UTF-8?q?=EB=A6=BF=EC=A7=80=20=EC=97=B0=EA=B2=B0=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/component/BtWebView.kt | 83 +++++++++++++------ .../boolti/presentation/screen/my/MyScreen.kt | 6 +- .../ShowRegistrationScreen.kt | 11 +-- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt index 8a8277fa..0c1b2c7e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt @@ -4,6 +4,7 @@ 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 @@ -49,9 +50,6 @@ class BtWebView @JvmOverloads constructor( setupSettings() setupWebViewClient() setupBridge() - /*evaluate("밍슈 하이~") { - Timber.tag("MANGBAAM-BtWebView()").d("웹 콜백: $it") - }*/ } @SuppressLint("SetJavaScriptEnabled") @@ -71,18 +69,26 @@ class BtWebView @JvmOverloads constructor( private fun setupBridge() { addJavascriptInterface( NativeBridge { response -> + Timber.tag("bridge").d("(WEB -> APP) $response") val data = response.toBridgeData() - _bridgeEvent.tryEmit(data).also { - Timber.tag("MANGBAAM-BtWebView(setupBridge)").d("성공? $it") - } + Timber.tag("bridge").d("(APP) 변환 후 데이터: $data") + _bridgeEvent.tryEmit(data) }, "boolti", ) } + /** + * 앱 -> 웹 데이터를 보내기 위한 함수 + * + * @param data 웹에 보낼 정보 + * @param result + */ fun evaluate(data: BridgeRequest, result: (String) -> Unit = {}) { val dataAsString = json.encodeToString(data) - evaluateJavascript("javascript:postMessage('$dataAsString')") { + Timber.tag("bridge").d("(APP -> WEB) $dataAsString") + evaluateJavascript("javascript:__boolti__webview__bridge__.postMessage($dataAsString)") { + Timber.tag("bridge").d("(APP -> WEB -> APP) 콜백: $it") result(it) } } @@ -94,7 +100,9 @@ class BtWebView @JvmOverloads constructor( webChromeClient = BtWebChromeClient( launchActivity = launchActivity, setFilePathCallback = setFilePathCallback, - onProgressChanged = { _progress.value = it }, + onProgressChanged = { + _progress.value = it + }, ) } } @@ -123,6 +131,12 @@ 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 + } } class NativeBridge(private val callback: (BridgeResponse) -> Unit = {}) { @@ -142,23 +156,38 @@ class NativeBridge(private val callback: (BridgeResponse) -> Unit = {}) { } } -enum class Command { - NAVIGATE_TO_SHOW_DETAIL, - NAVIGATE_BACK, - REQUEST_TOKEN, - UNKNOWN; +sealed interface Command { + @Serializable + enum class Receive : Command { + NAVIGATE_TO_SHOW_DETAIL, + NAVIGATE_BACK, + REQUEST_TOKEN, + UNKNOWN; + + companion object { + fun fromString(value: String): Receive = + Receive.entries.find { it.name == value.trim().uppercase() } ?: UNKNOWN + } + } - companion object { - fun fromString(value: String): Command = - Command.entries.find { it.name == value.trim().uppercase() } ?: UNKNOWN + @Serializable + enum class Send : Command { + REQUEST_TOKEN, + UNKNOWN; + + companion object { + fun fromString(value: String): Send = + Send.entries.find { it.name == value.trim().uppercase() } ?: UNKNOWN + } } } sealed interface BridgeData { - data class ShowDetail(val id: String) : BridgeData - data object NavigateBack : BridgeData - data object RequestToken : BridgeData - data object Unknown : BridgeData + val uuid: String + data class ShowDetail(override val uuid: String, val id: String) : BridgeData + data class NavigateBack(override val uuid: String) : BridgeData + data class RequestToken(override val uuid: String) : BridgeData + data class Unknown(override val uuid: String) : BridgeData } @Serializable @@ -168,8 +197,8 @@ data class BridgeResponse( val command: String, val data: JsonElement? = null, ) { - fun toBridgeData(): BridgeData = when (Command.fromString(command)) { - Command.NAVIGATE_TO_SHOW_DETAIL -> { + fun toBridgeData(): BridgeData = when (Command.Receive.fromString(command)) { + Command.Receive.NAVIGATE_TO_SHOW_DETAIL -> { val showId = when (data) { is JsonObject -> requireNotNull(data["showId"]?.toString()) { Timber.tag("MANGBAAM-BridgeResponse(toBridgeData)").d("showId is undefined") @@ -177,18 +206,18 @@ data class BridgeResponse( else -> throw IllegalArgumentException("data is not JsonObject") } - BridgeData.ShowDetail(showId) + BridgeData.ShowDetail(id, showId) } - Command.NAVIGATE_BACK -> BridgeData.NavigateBack - Command.REQUEST_TOKEN -> BridgeData.RequestToken - Command.UNKNOWN -> BridgeData.Unknown + Command.Receive.NAVIGATE_BACK -> BridgeData.NavigateBack(id) + Command.Receive.REQUEST_TOKEN -> BridgeData.RequestToken(id) + Command.Receive.UNKNOWN -> BridgeData.Unknown(id) } } @Serializable data class BridgeRequest( - val command: String, + val command: Command.Send, val data: String? = null, val id: String = UUID.randomUUID().toString(), val timestamp: String = System.currentTimeMillis().toString(), diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt index c3ba4ba9..4aafe2cd 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt @@ -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 @@ -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 = navigateToShowRegistration, onClickQrScan = if (user != null) onClickQrScan else requireLogin, ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt index 626afaa5..9ec236c1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt @@ -11,7 +11,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding @@ -46,7 +45,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import timber.log.Timber -@OptIn(ExperimentalLayoutApi::class) @SuppressLint("SetJavaScriptEnabled") @Composable fun ShowRegistrationScreen( @@ -129,20 +127,19 @@ fun ShowRegistrationScreen( } launch { bridgeEvent.collect { data -> - Timber.tag("MANGBAAM-(ShowRegistrationScreen)").d("$data") + Timber.tag("bridge").d("bridgeEvent 수집(ShowRegistrationScreen) - $data") when (data) { is BridgeData.RequestToken -> { evaluate( BridgeRequest( - command = Command.REQUEST_TOKEN.name, + id = data.uuid, + command = Command.Send.REQUEST_TOKEN, data = "hello", ) ) } - else -> { - Timber.tag("MANGBAAM-(ShowRegistrationScreen)").d("안 탔나?") - } + else -> Unit } } } From 74c57cd2fb4476c7d0fd975cd7a9f22e7c957623 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 15 Dec 2024 19:45:51 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feature/Boolti-347=20feat:=20=EB=B8=8C?= =?UTF-8?q?=EB=A6=BF=EC=A7=80=20=EC=BD=94=EB=93=9C=20=EA=B0=88=EC=95=84=20?= =?UTF-8?q?=EC=97=8E=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/component/BtWebView.kt | 167 ++++-------------- .../ShowRegistrationScreen.kt | 46 ++--- .../util/bridge/BridgeCallbackHandler.kt | 12 ++ .../presentation/util/bridge/BridgeDto.kt | 12 ++ .../presentation/util/bridge/BridgeManager.kt | 86 +++++++++ .../presentation/util/bridge/CommandType.kt | 16 ++ 6 files changed, 175 insertions(+), 164 deletions(-) create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeDto.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt create mode 100644 presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt index 0c1b2c7e..96df6f50 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/component/BtWebView.kt @@ -10,17 +10,13 @@ import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient -import kotlinx.coroutines.flow.MutableSharedFlow +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.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject import timber.log.Timber -import java.util.UUID class BtWebView @JvmOverloads constructor( context: Context, @@ -32,24 +28,12 @@ class BtWebView @JvmOverloads constructor( private val _progress = MutableStateFlow(0) val progress = _progress.asStateFlow() - private val _bridgeEvent = MutableSharedFlow( - extraBufferCapacity = 5, - ) - val bridgeEvent = _bridgeEvent.asSharedFlow() - - private val json = Json { - isLenient = true - prettyPrint = true - ignoreUnknownKeys = true - } - init { isFocusable = true isFocusableInTouchMode = true setupSettings() setupWebViewClient() - setupBridge() } @SuppressLint("SetJavaScriptEnabled") @@ -66,33 +50,6 @@ class BtWebView @JvmOverloads constructor( webViewClient = BtWebViewClient() } - private fun setupBridge() { - addJavascriptInterface( - NativeBridge { response -> - Timber.tag("bridge").d("(WEB -> APP) $response") - val data = response.toBridgeData() - Timber.tag("bridge").d("(APP) 변환 후 데이터: $data") - _bridgeEvent.tryEmit(data) - }, - "boolti", - ) - } - - /** - * 앱 -> 웹 데이터를 보내기 위한 함수 - * - * @param data 웹에 보낼 정보 - * @param result - */ - fun evaluate(data: BridgeRequest, result: (String) -> Unit = {}) { - val dataAsString = json.encodeToString(data) - Timber.tag("bridge").d("(APP -> WEB) $dataAsString") - evaluateJavascript("javascript:__boolti__webview__bridge__.postMessage($dataAsString)") { - Timber.tag("bridge").d("(APP -> WEB -> APP) 콜백: $it") - result(it) - } - } - fun setWebChromeClient( launchActivity: () -> Unit, setFilePathCallback: (ValueCallback>) -> Unit, @@ -100,10 +57,40 @@ class BtWebView @JvmOverloads constructor( webChromeClient = BtWebChromeClient( launchActivity = launchActivity, setFilePathCallback = setFilePathCallback, - onProgressChanged = { - _progress.value = it + 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") + } + } } } @@ -138,87 +125,3 @@ class BtWebChromeClient( return true } } - -class NativeBridge(private val callback: (BridgeResponse) -> Unit = {}) { - @JavascriptInterface - fun postMessage(jsonString: String) { - runCatching { json.decodeFromString(jsonString) } - .onSuccess(callback) - .onFailure { it.printStackTrace() } // TODO 에러 처리 - } - - companion object { - private val json = Json { - isLenient = true - prettyPrint = true - ignoreUnknownKeys = true - } - } -} - -sealed interface Command { - @Serializable - enum class Receive : Command { - NAVIGATE_TO_SHOW_DETAIL, - NAVIGATE_BACK, - REQUEST_TOKEN, - UNKNOWN; - - companion object { - fun fromString(value: String): Receive = - Receive.entries.find { it.name == value.trim().uppercase() } ?: UNKNOWN - } - } - - @Serializable - enum class Send : Command { - REQUEST_TOKEN, - UNKNOWN; - - companion object { - fun fromString(value: String): Send = - Send.entries.find { it.name == value.trim().uppercase() } ?: UNKNOWN - } - } -} - -sealed interface BridgeData { - val uuid: String - data class ShowDetail(override val uuid: String, val id: String) : BridgeData - data class NavigateBack(override val uuid: String) : BridgeData - data class RequestToken(override val uuid: String) : BridgeData - data class Unknown(override val uuid: String) : BridgeData -} - -@Serializable -data class BridgeResponse( - val id: String = UUID.randomUUID().toString(), - val timestamp: String = System.currentTimeMillis().toString(), - val command: String, - val data: JsonElement? = null, -) { - fun toBridgeData(): BridgeData = when (Command.Receive.fromString(command)) { - Command.Receive.NAVIGATE_TO_SHOW_DETAIL -> { - val showId = when (data) { - is JsonObject -> requireNotNull(data["showId"]?.toString()) { - Timber.tag("MANGBAAM-BridgeResponse(toBridgeData)").d("showId is undefined") - } - - else -> throw IllegalArgumentException("data is not JsonObject") - } - BridgeData.ShowDetail(id, showId) - } - - Command.Receive.NAVIGATE_BACK -> BridgeData.NavigateBack(id) - Command.Receive.REQUEST_TOKEN -> BridgeData.RequestToken(id) - Command.Receive.UNKNOWN -> BridgeData.Unknown(id) - } -} - -@Serializable -data class BridgeRequest( - val command: Command.Send, - val data: String? = null, - val id: String = UUID.randomUUID().toString(), - val timestamp: String = System.currentTimeMillis().toString(), -) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt index 9ec236c1..f1a4bf16 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt @@ -34,12 +34,12 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.nexters.boolti.presentation.BuildConfig import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.component.BTDialog -import com.nexters.boolti.presentation.component.BridgeData -import com.nexters.boolti.presentation.component.BridgeRequest import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.BtCircularProgressIndicator import com.nexters.boolti.presentation.component.BtWebView -import com.nexters.boolti.presentation.component.Command +import com.nexters.boolti.presentation.util.bridge.BridgeCallbackHandler +import com.nexters.boolti.presentation.util.bridge.BridgeManager +import com.nexters.boolti.presentation.util.bridge.TokenDto import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -68,6 +68,16 @@ fun ShowRegistrationScreen( var webviewProgress by remember { mutableIntStateOf(0) } val loading by remember { derivedStateOf { webviewProgress < 100 } } + LaunchedEffect(webView != null) { + webView?.setBridgeManager( + BridgeManager( + object : BridgeCallbackHandler { + override fun fetchToken(): TokenDto = TokenDto("test") + } + ) + ) + } + LaunchedEffect(Unit) { CookieManager.getInstance().removeAllCookies(null) WebStorage.getInstance().deleteAllData() @@ -81,14 +91,6 @@ fun ShowRegistrationScreen( setCookie(url, "x-refresh-token=${tokens.refreshToken}") flush() } - /*launch { - webView?.evaluate( - BridgeRequest( - command = Command.REQUEST_TOKEN.name, - data = tokens.accessToken, - ) - ) - }*/ } } @@ -122,27 +124,7 @@ fun ShowRegistrationScreen( Timber.d("내가 만든 쿠키 : ${CookieManager.getInstance().getCookie(url)}") scope.launch { - launch { - progress.collect { webviewProgress = it } - } - launch { - bridgeEvent.collect { data -> - Timber.tag("bridge").d("bridgeEvent 수집(ShowRegistrationScreen) - $data") - when (data) { - is BridgeData.RequestToken -> { - evaluate( - BridgeRequest( - id = data.uuid, - command = Command.Send.REQUEST_TOKEN, - data = "hello", - ) - ) - } - - else -> Unit - } - } - } + progress.collect { webviewProgress = it } } }.also { webView = it } }, diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt new file mode 100644 index 00000000..95186730 --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt @@ -0,0 +1,12 @@ +package com.nexters.boolti.presentation.util.bridge + +import kotlinx.serialization.Serializable + +interface BridgeCallbackHandler { + fun fetchToken(): TokenDto +} + +@Serializable +data class TokenDto( + val token: String, +) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeDto.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeDto.kt new file mode 100644 index 00000000..51eae6cd --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeDto.kt @@ -0,0 +1,12 @@ +package com.nexters.boolti.presentation.util.bridge + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class BridgeDto( + val id: String, + val command: CommandType, + val timestamp: Long = System.currentTimeMillis(), + val data: JsonElement? = null, +) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt new file mode 100644 index 00000000..df88226f --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -0,0 +1,86 @@ +package com.nexters.boolti.presentation.util.bridge + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.encodeToJsonElement + +class BridgeManager( + private val callbackHandler: BridgeCallbackHandler, + private val scope: CoroutineScope = CoroutineScope(SupervisorJob()), + val bridgeName: String = DEFAULT_BRIDGE_NAME, + private val webCallbackName: String = DEFAULT_WEB_CALLBACK_NAME, +) { + private val originDataMap = mutableMapOf() + private val _dataToSendToWeb = Channel() + val dataToSendWeb = _dataToSendToWeb.receiveAsFlow() + + fun handleIncomingData(data: BridgeDto) { + originDataMap[data.id] = data + + when (data.command) { + CommandType.REQUEST_TOKEN -> { + val token = callbackHandler.fetchToken() + callbackToWeb(data, json.encodeToJsonElement(token)) + } + + else -> callbackToWeb(data, null) + } + } + + fun sendDataToWeb(data: String) { + scope.launch { + _dataToSendToWeb.send("javascript:$webCallbackName($data)") + } + } + + private fun callbackToWeb( + originData: BridgeDto, + responseData: JsonElement? + ) { + val responseDto = originData.toBridgeCallbackDto( + data = responseData + ) + val json = json.encodeToString(responseDto) + sendDataToWeb(json) + } + + companion object { + private val json = Json { + isLenient = true + prettyPrint = true + ignoreUnknownKeys = true + } + + const val DEFAULT_BRIDGE_NAME = "boolti" + const val DEFAULT_WEB_CALLBACK_NAME = "__boolti__webview__bridge__.postMessage" + } +} + +@Serializable +private data class BridgeCallbackDto( + val id: String, + val command: CommandType, + val data: String, + val timestamp: Long = System.currentTimeMillis(), +) + +private fun BridgeDto.toBridgeCallbackDto(data: JsonElement? = null): BridgeCallbackDto { + val json = Json { + isLenient = true + ignoreUnknownKeys = true + } + val dataJsonElement = data ?: this.data ?: json.encodeToJsonElement(Unit) + return BridgeCallbackDto( + id = id, + command = command, + data = json.encodeToString(dataJsonElement), + timestamp = timestamp, + ) +} diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt new file mode 100644 index 00000000..ed728aeb --- /dev/null +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt @@ -0,0 +1,16 @@ +package com.nexters.boolti.presentation.util.bridge + +import kotlinx.serialization.Serializable + +@Serializable +enum class CommandType { + NAVIGATE_TO_SHOW_DETAIL, + NAVIGATE_BACK, + REQUEST_TOKEN, + UNKNOWN; + + companion object { + fun fromString(value: String): CommandType = + CommandType.entries.find { it.name == value.trim().uppercase() } ?: UNKNOWN + } +} From a4d86574eebd39ca86ef7b5f85ba78d9debafb8f Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 15 Dec 2024 22:56:17 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feature/Boolti-347=20docs:=20=EB=B8=8C?= =?UTF-8?q?=EB=A6=BF=EC=A7=80=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/bridge/BridgeCallbackHandler.kt | 5 +++ .../presentation/util/bridge/BridgeManager.kt | 31 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt index 95186730..b13ffe81 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt @@ -2,6 +2,11 @@ package com.nexters.boolti.presentation.util.bridge import kotlinx.serialization.Serializable +/** + * 브릿지를 통해 받은 데이터로부터 처리해야 하는 액션을 정의하는 핸들러 + * + * 새로운 액션이 정의되는 경우 [BridgeManager.handleIncomingData] 도 함께 수정되어야 한다. + */ interface BridgeCallbackHandler { fun fetchToken(): TokenDto } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt index df88226f..3069373a 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -21,8 +21,16 @@ class BridgeManager( private val _dataToSendToWeb = Channel() val dataToSendWeb = _dataToSendToWeb.receiveAsFlow() + /** + * 웹뷰에서 호출하는 메서드 + * + * 웹 브릿지를 통해 받은 데이터의 타입에 따라 [BridgeCallbackHandler]의 메서드를 호출한다. + * 이후 함수 내부에서 [BridgeCallbackHandler]의 메서드로부터 받은 결과를 웹으로 콜백한다. + * + * @param data + */ fun handleIncomingData(data: BridgeDto) { - originDataMap[data.id] = data + originDataMap[data.id] = data // 디버깅을 위한 캐싱 when (data.command) { CommandType.REQUEST_TOKEN -> { @@ -34,12 +42,27 @@ class BridgeManager( } } + /** + * WebView 에서 웹에 evaluation 할 자바스크립트 코드 전달 + * + * @param data + */ fun sendDataToWeb(data: String) { scope.launch { _dataToSendToWeb.send("javascript:$webCallbackName($data)") } } + /** + * 웹에서 브릿지를 통해 데이터를 받은 경우, 웹으로 콜백 해줘야 한다. (콜백 해주지 않으면 웹의 비동기 코드가 종료되지 않음) + * + * 예를 들어 화면 이동의 경우 웹으로 전달할 데이터가 없으니 [BridgeDto.timestamp]를 제외한 나머지 정보는 에코 방식으로 그대로 콜백한다. + * + * 토큰 요청과 같이 웹으로 전달할 데이터가 있는 경우 [BridgeDto.data]에 웹과 맞춘 형태로 데이터를 포함시키고, 나머지 [BridgeDto.id]와 [BridgeDto.command]는 그대로 콜백한다. + * + * @param originData 웹에서 브릿지를 통해 받은 원본 데이터 + * @param responseData [BridgeDto.data]에 포함될 데이터. `null` 이라면 원본 [BridgeDto.data] 를 사용한다. + */ private fun callbackToWeb( originData: BridgeDto, responseData: JsonElement? @@ -48,6 +71,8 @@ class BridgeManager( data = responseData ) val json = json.encodeToString(responseDto) + .replace("\"{\"", "{") + .replace("\"}\"", "}") sendDataToWeb(json) } @@ -67,8 +92,8 @@ class BridgeManager( private data class BridgeCallbackDto( val id: String, val command: CommandType, - val data: String, val timestamp: Long = System.currentTimeMillis(), + val data: String?, ) private fun BridgeDto.toBridgeCallbackDto(data: JsonElement? = null): BridgeCallbackDto { @@ -76,7 +101,7 @@ private fun BridgeDto.toBridgeCallbackDto(data: JsonElement? = null): BridgeCall isLenient = true ignoreUnknownKeys = true } - val dataJsonElement = data ?: this.data ?: json.encodeToJsonElement(Unit) + val dataJsonElement = data ?: this.data return BridgeCallbackDto( id = id, command = command, From c51f7eb2208bfb9303f70e085aa651684078c8c3 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Mon, 16 Dec 2024 00:29:27 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feature/Boolti-347=20feat:=20=EB=B8=8C?= =?UTF-8?q?=EB=A6=BF=EC=A7=80=20=EC=86=A1=EC=88=98=EC=8B=A0=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=ED=86=B5=EC=9D=BC...=20=EB=B8=8C=EB=A6=BF=EC=A7=80?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EC=A7=84=EC=A7=9C=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ShowRegistrationScreen.kt | 5 ++-- .../presentation/util/bridge/BridgeManager.kt | 30 ++----------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt index f1a4bf16..b44784e0 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt @@ -71,9 +71,10 @@ fun ShowRegistrationScreen( LaunchedEffect(webView != null) { webView?.setBridgeManager( BridgeManager( - object : BridgeCallbackHandler { + callbackHandler = object : BridgeCallbackHandler { override fun fetchToken(): TokenDto = TokenDto("test") - } + }, + scope = scope, ) ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt index 3069373a..469a57ea 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -67,12 +66,11 @@ class BridgeManager( originData: BridgeDto, responseData: JsonElement? ) { - val responseDto = originData.toBridgeCallbackDto( - data = responseData + val responseDto = originData.copy( + data = responseData, + timestamp = System.currentTimeMillis(), ) val json = json.encodeToString(responseDto) - .replace("\"{\"", "{") - .replace("\"}\"", "}") sendDataToWeb(json) } @@ -87,25 +85,3 @@ class BridgeManager( const val DEFAULT_WEB_CALLBACK_NAME = "__boolti__webview__bridge__.postMessage" } } - -@Serializable -private data class BridgeCallbackDto( - val id: String, - val command: CommandType, - val timestamp: Long = System.currentTimeMillis(), - val data: String?, -) - -private fun BridgeDto.toBridgeCallbackDto(data: JsonElement? = null): BridgeCallbackDto { - val json = Json { - isLenient = true - ignoreUnknownKeys = true - } - val dataJsonElement = data ?: this.data - return BridgeCallbackDto( - id = id, - command = command, - data = json.encodeToString(dataJsonElement), - timestamp = timestamp, - ) -} From 206d8a622c6a9e27700ad9320c9f6144a5e65d02 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Wed, 25 Dec 2024 18:23:51 +0900 Subject: [PATCH 06/13] =?UTF-8?q?Boolti-347=20feat:=20=EB=B8=8C=EB=A6=BF?= =?UTF-8?q?=EC=A7=80=20-=20=ED=99=94=EB=A9=B4=20=EC=9D=B4=EB=8F=99,=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=B1=EC=8B=A0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/Main.kt | 2 + .../ShowRegistrationNavigation.kt | 4 ++ .../ShowRegistrationScreen.kt | 38 +++++++++---------- .../ShowRegistrationViewModel.kt | 10 +---- .../util/bridge/BridgeCallbackHandler.kt | 7 +++- .../presentation/util/bridge/BridgeManager.kt | 26 ++++++++++++- 6 files changed, 56 insertions(+), 31 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt index 32c7e706..6d4e1319 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/Main.kt @@ -298,6 +298,8 @@ fun MainNavigation(modifier: Modifier, onClickQrScan: (showId: String, showName: addShowRegistration( modifier = modifier, popBackStack = navController::popBackStack, + navigateTo = navController::navigateTo, + navigateToHome = navController::navigateToHome, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt index 991e4659..44cc9577 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt @@ -8,6 +8,8 @@ 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, @@ -15,6 +17,8 @@ fun NavGraphBuilder.addShowRegistration( ShowRegistrationScreen( modifier = modifier, onClickBack = popBackStack, + navigateTo = navigateTo, + navigateToHome = navigateToHome, ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt index b44784e0..4d021a2b 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.net.Uri import android.webkit.CookieManager import android.webkit.ValueCallback -import android.webkit.WebStorage import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -39,9 +38,8 @@ import com.nexters.boolti.presentation.component.BtCircularProgressIndicator import com.nexters.boolti.presentation.component.BtWebView import com.nexters.boolti.presentation.util.bridge.BridgeCallbackHandler import com.nexters.boolti.presentation.util.bridge.BridgeManager +import com.nexters.boolti.presentation.util.bridge.NavigateOption import com.nexters.boolti.presentation.util.bridge.TokenDto -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import timber.log.Timber @@ -50,6 +48,8 @@ import timber.log.Timber fun ShowRegistrationScreen( modifier: Modifier = Modifier, onClickBack: () -> Unit, + navigateTo: (route: String) -> Unit, + navigateToHome: () -> Unit, viewModel: ShowRegistrationViewModel = hiltViewModel(), ) { var filePathCallback: ValueCallback>? by remember { mutableStateOf(null) } @@ -72,29 +72,27 @@ fun ShowRegistrationScreen( webView?.setBridgeManager( BridgeManager( callbackHandler = object : BridgeCallbackHandler { - override fun fetchToken(): TokenDto = TokenDto("test") + override suspend fun fetchToken(): TokenDto { + val accessToken = viewModel.refreshAndGetToken() + return TokenDto(token = accessToken.token) + } + + override fun navigateTo(route: String, navigateOption: NavigateOption) { + when (navigateOption) { + NavigateOption.PUSH -> navigateTo(route) + NavigateOption.HOME -> navigateToHome() + NavigateOption.CLOSE_AND_OPEN -> { + onClickBack() + navigateTo(route) + } + } + } }, scope = scope, ) ) } - LaunchedEffect(Unit) { - CookieManager.getInstance().removeAllCookies(null) - WebStorage.getInstance().deleteAllData() - - viewModel.tokens - .filterNotNull() - .filter { it.isLoggedIn } - .collect { tokens -> - with(CookieManager.getInstance()) { - setCookie(url, "x-access-token=${tokens.accessToken}") - setCookie(url, "x-refresh-token=${tokens.refreshToken}") - flush() - } - } - } - BackHandler { if (webView?.canGoBack() == true) webView?.goBack() else showExitDialog = true } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt index 8861822e..040530e5 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationViewModel.kt @@ -1,20 +1,14 @@ package com.nexters.boolti.presentation.screen.showregistration import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.nexters.boolti.domain.repository.AuthRepository -import com.nexters.boolti.presentation.extension.stateInUi import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first import javax.inject.Inject @HiltViewModel class ShowRegistrationViewModel @Inject constructor( val authRepository: AuthRepository, ) : ViewModel() { - val tokens = authRepository.getTokens() - .stateInUi(viewModelScope, null) - - fun refreshToken() { - authRepository.refreshToken() - } + suspend fun refreshAndGetToken() = authRepository.refreshToken().first() } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt index b13ffe81..69cc25f1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt @@ -8,7 +8,12 @@ import kotlinx.serialization.Serializable * 새로운 액션이 정의되는 경우 [BridgeManager.handleIncomingData] 도 함께 수정되어야 한다. */ interface BridgeCallbackHandler { - fun fetchToken(): TokenDto + suspend fun fetchToken(): TokenDto + fun navigateTo(route: String, navigateOption: NavigateOption = NavigateOption.PUSH) +} + +enum class NavigateOption { + PUSH, HOME, CLOSE_AND_OPEN, } @Serializable diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt index 469a57ea..7e67b9d8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -1,14 +1,19 @@ package com.nexters.boolti.presentation.util.bridge +import com.nexters.boolti.presentation.screen.MainDestination import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject +import timber.log.Timber class BridgeManager( private val callbackHandler: BridgeCallbackHandler, @@ -33,8 +38,25 @@ class BridgeManager( when (data.command) { CommandType.REQUEST_TOKEN -> { - val token = callbackHandler.fetchToken() - callbackToWeb(data, json.encodeToJsonElement(token)) + scope.launch { + val token = callbackHandler.fetchToken() + callbackToWeb(data, json.encodeToJsonElement(token)) + } + } + + CommandType.NAVIGATE_TO_SHOW_DETAIL -> { + data.data?.jsonObject?.get("showId")?.toString()?.let { showId -> + scope.launch { + withContext(Dispatchers.Main) { + Timber.tag("bridge").d("공연 상세 화면으로 이동 $showId") + callbackHandler.navigateTo( + route = MainDestination.ShowDetail.createRoute(showId), + navigateOption = NavigateOption.CLOSE_AND_OPEN, + ) + } + } + } ?: Timber.tag("bridge").d("공연 상세 화면으로 이동 실패: showId 없음") + callbackToWeb(data, null) } else -> callbackToWeb(data, null) From 7ff99bafb338d61d7f35e1bab8c0da6ec4f22801 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Wed, 25 Dec 2024 18:26:54 +0900 Subject: [PATCH 07/13] =?UTF-8?q?Boolti-347=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=9C=A0=EC=A0=80=EB=A7=8C=20=EA=B3=B5=EC=97=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nexters/boolti/presentation/screen/my/MyScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt index 4aafe2cd..5678c1d0 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt @@ -77,7 +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 = navigateToShowRegistration, + onClickRegisterShow = if (user != null) navigateToShowRegistration else requireLogin, onClickQrScan = if (user != null) onClickQrScan else requireLogin, ) } From 849f31be4ad5b2d56372617f83524a21667f01c3 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Tue, 31 Dec 2024 02:00:11 +0900 Subject: [PATCH 08/13] =?UTF-8?q?Boolti-343=20fix:=20=EB=A8=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nexters/boolti/presentation/screen/my/MyScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt index 8592dfd0..4aa616cc 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/my/MyScreen.kt @@ -1,5 +1,6 @@ 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 @@ -63,7 +64,6 @@ fun MyScreen( val user by viewModel.user.collectAsStateWithLifecycle() val domain = BuildConfig.DOMAIN - val registrationUrl = "https://${domain}/show/add" val homeUrl = "https://${domain}/home" val uriHandler = LocalUriHandler.current val context = LocalContext.current From c5050231750dcd6b2119229a51df0fbbd8c76bec Mon Sep 17 00:00:00 2001 From: mangbaam Date: Tue, 31 Dec 2024 23:33:33 +0900 Subject: [PATCH 09/13] =?UTF-8?q?feature/Boolti-347=20=ED=99=88=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=8F=84=20=EC=9D=B8=EC=95=B1=20=EA=B3=B5=EC=97=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/screen/home/HomeScreen.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt index 42f5dc38..8f5c4978 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/home/HomeScreen.kt @@ -2,7 +2,6 @@ package com.nexters.boolti.presentation.screen.home import android.content.Intent import android.net.Uri -import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.layout.Column @@ -25,7 +24,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -36,7 +34,6 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navDeepLink -import com.nexters.boolti.presentation.BuildConfig import com.nexters.boolti.presentation.R import com.nexters.boolti.presentation.extension.requireActivity import com.nexters.boolti.presentation.screen.LocalSnackbarController @@ -69,9 +66,6 @@ fun HomeScreen( val currentDestination = navBackStackEntry?.destination?.route ?: Destination.Show.route val loggedIn by viewModel.loggedIn.collectAsStateWithLifecycle() - val domain = BuildConfig.DOMAIN - val url = "https://${domain}/show/add" - val uriHandler = LocalUriHandler.current val context = LocalContext.current val giftRegistrationMessage = stringResource(id = R.string.gift_successfully_registered) @@ -141,10 +135,7 @@ fun HomeScreen( modifier = modifier.padding(innerPadding), onClickShowItem = onClickShowItem, navigateToBusiness = navigateToBusiness, - navigateToShowRegistration = { - uriHandler.openUri(url) - Toast.makeText(context, "공연 등록을 위해 웹으로 이동합니다", Toast.LENGTH_LONG).show() - } // navigateToShowRegistration, // TODO 추후 인앱 공연 등록 반영 시 주석 해제 + navigateToShowRegistration = navigateToShowRegistration, ) } composable( From 6c4fa081a4d86f7df463a461a58bb516f1453087 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Wed, 1 Jan 2025 11:59:55 +0900 Subject: [PATCH 10/13] =?UTF-8?q?feature/Boolti-347=20=EB=B8=8C=EB=A6=BF?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=8A=A4=EB=82=B5=EB=B0=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ShowRegistrationScreen.kt | 8 +++++++ .../presentation/util/SnackbarController.kt | 4 +++- .../util/bridge/BridgeCallbackHandler.kt | 2 ++ .../presentation/util/bridge/BridgeManager.kt | 21 +++++++++++++++++++ .../presentation/util/bridge/CommandType.kt | 1 + 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt index 4d021a2b..964b3c39 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -36,6 +37,7 @@ import com.nexters.boolti.presentation.component.BTDialog import com.nexters.boolti.presentation.component.BtBackAppBar import com.nexters.boolti.presentation.component.BtCircularProgressIndicator import com.nexters.boolti.presentation.component.BtWebView +import com.nexters.boolti.presentation.screen.LocalSnackbarController import com.nexters.boolti.presentation.util.bridge.BridgeCallbackHandler import com.nexters.boolti.presentation.util.bridge.BridgeManager import com.nexters.boolti.presentation.util.bridge.NavigateOption @@ -68,6 +70,8 @@ fun ShowRegistrationScreen( var webviewProgress by remember { mutableIntStateOf(0) } val loading by remember { derivedStateOf { webviewProgress < 100 } } + val snackbarHostState = LocalSnackbarController.current + LaunchedEffect(webView != null) { webView?.setBridgeManager( BridgeManager( @@ -87,6 +91,10 @@ fun ShowRegistrationScreen( } } } + + override fun showSnackbar(message: String, duration: SnackbarDuration) { + snackbarHostState.showMessage(message = message, duration = duration) + } }, scope = scope, ) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt index 3e388b7c..9666b017 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/SnackbarController.kt @@ -1,5 +1,6 @@ package com.nexters.boolti.presentation.util +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -12,10 +13,11 @@ class SnackbarController( fun showMessage( message: String, dismissPrevious: Boolean = true, + duration: SnackbarDuration = SnackbarDuration.Short, ) { coroutineScope.launch { if (dismissPrevious) snackbarHostState.currentSnackbarData?.dismiss() - snackbarHostState.showSnackbar(message) + snackbarHostState.showSnackbar(message, duration = duration) } } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt index 69cc25f1..0d57ecac 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt @@ -1,5 +1,6 @@ package com.nexters.boolti.presentation.util.bridge +import androidx.compose.material3.SnackbarDuration import kotlinx.serialization.Serializable /** @@ -10,6 +11,7 @@ import kotlinx.serialization.Serializable interface BridgeCallbackHandler { suspend fun fetchToken(): TokenDto fun navigateTo(route: String, navigateOption: NavigateOption = NavigateOption.PUSH) + fun showSnackbar(message: String, duration: SnackbarDuration = SnackbarDuration.Short) } enum class NavigateOption { diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt index 7e67b9d8..b33d5086 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -1,5 +1,6 @@ package com.nexters.boolti.presentation.util.bridge +import androidx.compose.material3.SnackbarDuration import com.nexters.boolti.presentation.screen.MainDestination import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -59,6 +60,26 @@ class BridgeManager( callbackToWeb(data, null) } + CommandType.SHOW_TOAST -> { + data.data?.jsonObject?.let { + val message = it["message"]?.toString() ?: run { + Timber.tag("bridge").d("토스트 메시지 출력 실패: message 없음") + callbackToWeb(data, null) + return@let + } + val duration = it["duration"]?.toString()?.let { durationStr -> + if (durationStr.equals("long", true)) { + SnackbarDuration.Long + } else { + SnackbarDuration.Short + } + } ?: SnackbarDuration.Short + + callbackHandler.showSnackbar(message, duration) + } + callbackToWeb(data, null) + } + else -> callbackToWeb(data, null) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt index ed728aeb..ca2e88aa 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/CommandType.kt @@ -7,6 +7,7 @@ enum class CommandType { NAVIGATE_TO_SHOW_DETAIL, NAVIGATE_BACK, REQUEST_TOKEN, + SHOW_TOAST, UNKNOWN; companion object { From 37fef6e5178352cb2797659ec3c2a5b967870e70 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Wed, 1 Jan 2025 17:48:05 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feature/Boolti-347=20=EB=B8=8C=EB=A6=BF?= =?UTF-8?q?=EC=A7=80=20=EC=BD=9C=EB=B0=B1=EC=97=90=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B0=98=EC=98=81=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boolti/presentation/util/bridge/BridgeManager.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt index b33d5086..570b841e 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -57,14 +57,14 @@ class BridgeManager( } } } ?: Timber.tag("bridge").d("공연 상세 화면으로 이동 실패: showId 없음") - callbackToWeb(data, null) + callbackToWeb(data) } CommandType.SHOW_TOAST -> { data.data?.jsonObject?.let { val message = it["message"]?.toString() ?: run { Timber.tag("bridge").d("토스트 메시지 출력 실패: message 없음") - callbackToWeb(data, null) + callbackToWeb(data) return@let } val duration = it["duration"]?.toString()?.let { durationStr -> @@ -77,10 +77,10 @@ class BridgeManager( callbackHandler.showSnackbar(message, duration) } - callbackToWeb(data, null) + callbackToWeb(data) } - else -> callbackToWeb(data, null) + else -> callbackToWeb(data) } } @@ -107,10 +107,10 @@ class BridgeManager( */ private fun callbackToWeb( originData: BridgeDto, - responseData: JsonElement? + responseData: JsonElement? = null, ) { val responseDto = originData.copy( - data = responseData, + data = responseData ?: originData.data, timestamp = System.currentTimeMillis(), ) val json = json.encodeToString(responseDto) From 18ad02fbc95ea9dac7d757c9b592e2e9b149efd3 Mon Sep 17 00:00:00 2001 From: mangbaam Date: Wed, 1 Jan 2025 18:15:18 +0900 Subject: [PATCH 12/13] =?UTF-8?q?feature/Boolti-347=20=EB=A8=B8=EC=A7=80?= =?UTF-8?q?=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=202.8.x=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/showregistration/ShowRegistrationNavigation.kt | 1 + .../screen/showregistration/ShowRegistrationScreen.kt | 4 ++-- .../presentation/util/bridge/BridgeCallbackHandler.kt | 2 +- .../boolti/presentation/util/bridge/BridgeManager.kt | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt index 62d9a5dd..a3809475 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationNavigation.kt @@ -17,6 +17,7 @@ fun NavGraphBuilder.addShowRegistration( ShowRegistrationScreen( modifier = modifier, onClickBack = navController::popBackStack, + navigateTo = navController::navigate, navigateToHome = navController::navigateToHome, ) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt index 964b3c39..be4f0713 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/showregistration/ShowRegistrationScreen.kt @@ -50,7 +50,7 @@ import timber.log.Timber fun ShowRegistrationScreen( modifier: Modifier = Modifier, onClickBack: () -> Unit, - navigateTo: (route: String) -> Unit, + navigateTo: (route: Any) -> Unit, navigateToHome: () -> Unit, viewModel: ShowRegistrationViewModel = hiltViewModel(), ) { @@ -81,7 +81,7 @@ fun ShowRegistrationScreen( return TokenDto(token = accessToken.token) } - override fun navigateTo(route: String, navigateOption: NavigateOption) { + override fun navigate(route: T, navigateOption: NavigateOption) { when (navigateOption) { NavigateOption.PUSH -> navigateTo(route) NavigateOption.HOME -> navigateToHome() diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt index 0d57ecac..e1f5021f 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeCallbackHandler.kt @@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable */ interface BridgeCallbackHandler { suspend fun fetchToken(): TokenDto - fun navigateTo(route: String, navigateOption: NavigateOption = NavigateOption.PUSH) + fun navigate(route: T, navigateOption: NavigateOption = NavigateOption.PUSH) fun showSnackbar(message: String, duration: SnackbarDuration = SnackbarDuration.Short) } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt index 570b841e..a6f6d9e1 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -1,7 +1,7 @@ package com.nexters.boolti.presentation.util.bridge import androidx.compose.material3.SnackbarDuration -import com.nexters.boolti.presentation.screen.MainDestination +import com.nexters.boolti.presentation.screen.navigation.ShowRoute import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -50,8 +50,8 @@ class BridgeManager( scope.launch { withContext(Dispatchers.Main) { Timber.tag("bridge").d("공연 상세 화면으로 이동 $showId") - callbackHandler.navigateTo( - route = MainDestination.ShowDetail.createRoute(showId), + callbackHandler.navigate( + route = ShowRoute.ShowRoot(showId), navigateOption = NavigateOption.CLOSE_AND_OPEN, ) } From 23d7f5396aeffac899ae2cdf518c5b3ca4aff8bc Mon Sep 17 00:00:00 2001 From: mangbaam Date: Sun, 12 Jan 2025 00:45:35 +0900 Subject: [PATCH 13/13] =?UTF-8?q?Boolti-347=20pr=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/util/bridge/BridgeManager.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt index a6f6d9e1..89602eb6 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/util/bridge/BridgeManager.kt @@ -1,14 +1,14 @@ package com.nexters.boolti.presentation.util.bridge +import android.os.Handler +import android.os.Looper import androidx.compose.material3.SnackbarDuration import com.nexters.boolti.presentation.screen.navigation.ShowRoute import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -47,14 +47,12 @@ class BridgeManager( CommandType.NAVIGATE_TO_SHOW_DETAIL -> { data.data?.jsonObject?.get("showId")?.toString()?.let { showId -> - scope.launch { - withContext(Dispatchers.Main) { - Timber.tag("bridge").d("공연 상세 화면으로 이동 $showId") - callbackHandler.navigate( - route = ShowRoute.ShowRoot(showId), - navigateOption = NavigateOption.CLOSE_AND_OPEN, - ) - } + Handler(Looper.getMainLooper()).post { + Timber.tag("bridge").d("공연 상세 화면으로 이동 $showId") + callbackHandler.navigate( + route = ShowRoute.ShowRoot(showId), + navigateOption = NavigateOption.CLOSE_AND_OPEN, + ) } } ?: Timber.tag("bridge").d("공연 상세 화면으로 이동 실패: showId 없음") callbackToWeb(data) @@ -80,7 +78,8 @@ class BridgeManager( callbackToWeb(data) } - else -> callbackToWeb(data) + CommandType.NAVIGATE_BACK -> callbackToWeb(data) + CommandType.UNKNOWN -> callbackToWeb(data) } }