Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@
android:name=".presentation.scheme.SchemeActivity"
android:exported="true">

<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="rnnt.page.link"
android:host="runnect-ios.web.app"
android:pathPrefix="/share"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
android:scheme="https" />
</intent-filter>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.runnect.runnect.presentation.detail
import android.app.Activity
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
Expand All @@ -13,8 +12,6 @@ import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import coil3.load
import com.google.firebase.dynamiclinks.DynamicLink
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
import com.naver.maps.geometry.LatLng
import com.runnect.runnect.R
import com.runnect.runnect.binding.BindingActivity
Expand Down Expand Up @@ -50,7 +47,7 @@ import com.runnect.runnect.util.custom.dialog.RequireLoginDialogFragment
import com.runnect.runnect.util.custom.popup.PopupItem
import com.runnect.runnect.util.custom.popup.RunnectPopupMenu
import com.runnect.runnect.util.custom.toast.RunnectToast
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink
import com.runnect.runnect.util.link.RunnectShareLink
import com.runnect.runnect.util.extension.applyScreenEnterAnimation
import com.runnect.runnect.util.extension.applyScreenExitAnimation
import com.runnect.runnect.util.extension.getCompatibleSerializableExtra
Expand Down Expand Up @@ -112,7 +109,7 @@ class CourseDetailActivity :
}

private fun initCourseIdExtra() {
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_DYNAMIC_LINK, -1)
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_APP_LINK, -1)
if (idFromLink != -1) {
isFromDeepLink = true
publicCourseId = idFromLink
Expand Down Expand Up @@ -230,49 +227,17 @@ class CourseDetailActivity :

private fun initShareButtonClickListener() {
binding.btnShare.setOnClickListener {
sendFirebaseDynamicLink(
title = courseDetail.title,
desc = courseDetail.description,
image = courseDetail.image
)
shareLink("${RunnectShareLink.BASE_URL}?${RunnectShareLink.KEY_PUBLIC_COURSE_ID}=$publicCourseId")
Analytics.logClickedItemEvent(EVENT_CLICK_SHARE)
}
}

private fun sendFirebaseDynamicLink(title: String, desc: String, image: String) {
val link =
"${RunnectDynamicLink.BASE_URL}/?${RunnectDynamicLink.KEY_PUBLIC_COURSE_ID}=$publicCourseId"

FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse(link))
.setDomainUriPrefix(RunnectDynamicLink.BASE_URL)
.setAndroidParameters(DynamicLink.AndroidParameters.Builder().build())
.setIosParameters(
DynamicLink.IosParameters.Builder(RunnectDynamicLink.IOS_BUNDLE_ID).build()
)
.setSocialMetaTagParameters(
DynamicLink.SocialMetaTagParameters.Builder()
.setTitle(title)
.setDescription(desc)
.setImageUrl(Uri.parse(image))
.build()
)
.buildShortDynamicLink()
.addOnSuccessListener { result ->
val shortLink = result.shortLink
shareLink(shortLink.toString())
}
.addOnFailureListener {
it.printStackTrace()
}
}

private fun shareLink(url: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = RunnectDynamicLink.SEND_INTENT_MIME_TYPE
type = RunnectShareLink.SEND_INTENT_MIME_TYPE
putExtra(Intent.EXTRA_TEXT, url)
}
startActivity(Intent.createChooser(intent, RunnectDynamicLink.INTENT_CHOOSER_TITLE))
startActivity(Intent.createChooser(intent, RunnectShareLink.INTENT_CHOOSER_TITLE))
}

private fun initShowMoreButtonClickListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import android.net.Uri
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
import com.runnect.runnect.application.PreferenceManager
import com.runnect.runnect.presentation.detail.CourseDetailActivity
import com.runnect.runnect.presentation.login.LoginActivity
import com.runnect.runnect.presentation.storage.mydrawdetail.MyDrawDetailActivity
import com.runnect.runnect.util.analytics.Analytics
import com.runnect.runnect.util.analytics.EventName
import com.runnect.runnect.util.analytics.EventName.Param
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink.KEY_PRIVATE_COURSE_ID
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink.KEY_PUBLIC_COURSE_ID
import com.runnect.runnect.util.link.RunnectShareLink.KEY_PRIVATE_COURSE_ID
import com.runnect.runnect.util.link.RunnectShareLink.KEY_PUBLIC_COURSE_ID
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber

Expand All @@ -26,7 +25,7 @@ class SchemeActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)

if (isUserLoggedIn()) {
handleDynamicLinks()
handleAppLink()
} else {
navigateToLoginScreen()
Comment on lines 27 to 30

@coderabbitai coderabbitai Bot Aug 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve the App Link through the login flow.

When the user is logged out, this branch starts LoginActivity without the incoming URI or parsed target. After authentication, the user cannot continue to the shared course.

Pass the App Link to the login flow and route it after login succeeds. Apply the same behavior to public and private course links if login remains required for both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/runnect/runnect/presentation/scheme/SchemeActivity.kt`
around lines 27 - 30, Update the logged-out branch in SchemeActivity so
navigateToLoginScreen carries the incoming App Link URI or parsed target into
LoginActivity. Ensure LoginActivity preserves that value and routes to the
target after authentication succeeds, applying the same handling for both public
and private course links when login is required.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 코드 유지 (범위 밖으로 보류)

지적 자체는 타당하지만, 이 PR이 만든 회귀가 아니라 마이그레이션 이전부터 있던 동작입니다. origin/develop의 마이그레이션 전 SchemeActivity.kt도 동일하게 로그아웃 상태면 링크 정보 없이 LoginActivity로 보냅니다:

git show origin/develop:app/src/main/java/com/runnect/runnect/presentation/scheme/SchemeActivity.kt
...
        if (isUserLoggedIn()) {
            handleDynamicLinks()
        } else {
            navigateToLoginScreen()   // 링크 정보 전달 없음, 이전부터 동일
        }

이번 PR 범위는 Firebase Dynamic Links → App Links 전환이고 navigateToLoginScreen() 자체는 손대지 않았습니다. 로그인 플로우를 거쳐도 딥링크를 보존하는 건 legit한 UX 개선이지만 별도 작업으로 트래킹하는 게 맞다고 판단해 이 PR에서는 다루지 않았습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@unam98, 확인했습니다. 이 동작은 이번 App Links 전환에서 새로 발생한 회귀가 아닙니다.

현재 PR의 범위를 고려하면 이 코멘트는 블로킹하지 않겠습니다. 로그인 후 App Link를 복원하는 개선은 별도 작업으로 추적하는 것이 적절합니다.

원하시면 PR과 이 코멘트를 연결한 후속 GitHub Issue를 생성하겠습니다.

You are interacting with an AI system.

}
Expand All @@ -37,54 +36,45 @@ class SchemeActivity : AppCompatActivity() {
return accessToken != "none"
}

private fun handleDynamicLinks() {
FirebaseDynamicLinks.getInstance().getDynamicLink(intent)
.addOnSuccessListener(this) { pendingDynamicLinkData ->
val link = pendingDynamicLinkData.link

if (link != null) {
val publicCourseId = getCourseId(link, KEY_PUBLIC_COURSE_ID)
val privateCourseId = getCourseId(link, KEY_PRIVATE_COURSE_ID)

val targetScreen = when {
publicCourseId != null -> "CourseDetail"
privateCourseId != null -> "MyDrawDetail"
else -> "unknown"
}
Analytics.logEvent(
EventName.SYS_DEEPLINK_OPEN,
Param.DEEPLINK_URL to link.toString(),
Param.TARGET_SCREEN to targetScreen
)
private fun handleAppLink() {
val link = intent.data
if (link == null) {
Timber.e("FAIL: no data in intent")
finish()
return
}

when {
publicCourseId != null -> navigateToCourseDetail<CourseDetailActivity>(
publicCourseId
)
val publicCourseId = getCourseId(link, KEY_PUBLIC_COURSE_ID)
val privateCourseId = getCourseId(link, KEY_PRIVATE_COURSE_ID)

privateCourseId != null -> navigateToCourseDetail<MyDrawDetailActivity>(
privateCourseId
)
val targetScreen = when {
publicCourseId != null -> "CourseDetail"
privateCourseId != null -> "MyDrawDetail"
else -> "unknown"
}
Analytics.logEvent(
EventName.SYS_DEEPLINK_OPEN,
Param.DEEPLINK_URL to link.toString(),
Param.TARGET_SCREEN to targetScreen
)

else -> {
Timber.e("FAIL: could not find course id")
finish()
}
}
}
}
.addOnFailureListener(this) { t ->
Timber.e("FAIL: getDynamicLink from intent : ${t.message}")
when {
publicCourseId != null -> navigateToCourseDetail<CourseDetailActivity>(publicCourseId)
privateCourseId != null -> navigateToCourseDetail<MyDrawDetailActivity>(privateCourseId)
else -> {
Timber.e("FAIL: could not find course id")
finish()
}
}
}

private fun getCourseId(link: Uri, key: String): Int? {
return link.getQueryParameter(key)?.toInt()
return link.getQueryParameter(key)?.toIntOrNull()
}

private inline fun <reified T : Activity> navigateToCourseDetail(courseId: Int) {
Intent(this, T::class.java).apply {
putExtra(EXTRA_FROM_DYNAMIC_LINK, courseId)
putExtra(EXTRA_FROM_APP_LINK, courseId)
startActivity(this)
}
}
Expand All @@ -97,7 +87,7 @@ class SchemeActivity : AppCompatActivity() {
}

companion object {
const val EXTRA_FROM_DYNAMIC_LINK = "fromDynamicLink"
const val EXTRA_FROM_APP_LINK = "fromAppLink"
private const val TOKEN_KEY_ACCESS = "access"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.runnect.runnect.presentation.storage.mydrawdetail

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
Expand All @@ -12,8 +11,6 @@ import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.firebase.dynamiclinks.DynamicLink
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
import com.naver.maps.geometry.LatLng
import com.runnect.runnect.R
import com.runnect.runnect.binding.BindingActivity
Expand All @@ -35,7 +32,7 @@ import com.runnect.runnect.util.custom.toolbar.ToolbarMenu
import com.runnect.runnect.util.analytics.Analytics
import com.runnect.runnect.util.analytics.EventName
import com.runnect.runnect.util.analytics.EventName.Param
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink
import com.runnect.runnect.util.link.RunnectShareLink
import com.runnect.runnect.util.extension.PermissionUtil
import com.runnect.runnect.util.extension.applyScreenExitAnimation
import com.runnect.runnect.util.extension.hideKeyboard
Expand All @@ -44,7 +41,6 @@ import com.runnect.runnect.util.extension.showSnackbar
import com.runnect.runnect.util.extension.showToast
import com.runnect.runnect.util.extension.showWebBrowser
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber

@AndroidEntryPoint
class MyDrawDetailActivity :
Expand All @@ -56,7 +52,7 @@ class MyDrawDetailActivity :
private val touchList = arrayListOf<LatLng>()
private val selectList = arrayListOf<Int>()
private var courseId = -1
private var isFromDynamicLink = false
private var isFromAppLink = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -74,9 +70,9 @@ class MyDrawDetailActivity :
}

private fun initCourseIdExtra() {
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_DYNAMIC_LINK, -1)
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_APP_LINK, -1)
if (idFromLink != -1) {
isFromDynamicLink = true
isFromAppLink = true
courseId = idFromLink
return
}
Expand Down Expand Up @@ -283,7 +279,7 @@ class MyDrawDetailActivity :
}

private fun initBackButtonClickListener() {
if (isFromDynamicLink) {
if (isFromAppLink) {
navigateToMainScreen()
return
}
Expand All @@ -292,7 +288,7 @@ class MyDrawDetailActivity :
}

private fun addRightMenu() {
if (isFromDynamicLink) {
if (isFromAppLink) {
if (myDrawCourseDetail.isNowUser) {
addShareEditDeleteMenu()
}
Expand Down Expand Up @@ -334,43 +330,12 @@ class MyDrawDetailActivity :
}

private fun initShareButtonClickListener() {
createDynamicLink(
title = viewModel.courseTitle,
imgUrl = myDrawCourseDetail.imgUrl
)
}

private fun createDynamicLink(title: String, imgUrl: String) {
val link = "${RunnectDynamicLink.BASE_URL}/?${RunnectDynamicLink.KEY_PRIVATE_COURSE_ID}=${courseId}"
FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse(link))
.setDomainUriPrefix(RunnectDynamicLink.BASE_URL)
.setAndroidParameters(DynamicLink.AndroidParameters.Builder().build())
.setIosParameters(
DynamicLink.IosParameters.Builder(RunnectDynamicLink.IOS_BUNDLE_ID).build()
)
.setSocialMetaTagParameters(
DynamicLink.SocialMetaTagParameters.Builder()
.setTitle(title)
.setImageUrl(Uri.parse(imgUrl))
.build()
)
.buildShortDynamicLink()
.addOnSuccessListener { result ->
Timber.d("shortLink: ${result.shortLink}")
shareDynamicLink(result.shortLink.toString())
}
.addOnFailureListener { t ->
Timber.e("sendDynamicLink fail: ${t.message}")
}
}

private fun shareDynamicLink(shortLink: String) {
val url = "${RunnectShareLink.BASE_URL}?${RunnectShareLink.KEY_PRIVATE_COURSE_ID}=$courseId"
val intent = Intent(Intent.ACTION_SEND).apply {
type = RunnectDynamicLink.SEND_INTENT_MIME_TYPE
putExtra(Intent.EXTRA_TEXT, shortLink)
type = RunnectShareLink.SEND_INTENT_MIME_TYPE
putExtra(Intent.EXTRA_TEXT, url)
}
startActivity(Intent.createChooser(intent, RunnectDynamicLink.INTENT_CHOOSER_TITLE))
startActivity(Intent.createChooser(intent, RunnectShareLink.INTENT_CHOOSER_TITLE))
}

private fun createTitleEditBottomSheet(): BottomSheetDialog {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.runnect.runnect.util.dynamiclink
package com.runnect.runnect.util.link

object RunnectDynamicLink {
const val BASE_URL = "https://rnnt.page.link"
const val IOS_BUNDLE_ID = "com.runnect.Runnect-iOS"
object RunnectShareLink {
const val BASE_URL = "https://runnect-ios.web.app/share"
const val SEND_INTENT_MIME_TYPE = "text/plain"
const val INTENT_CHOOSER_TITLE = "Share Link"
const val KEY_PUBLIC_COURSE_ID = "courseId"
Expand Down
2 changes: 0 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp"
# Firebase (BOM 관리 - 개별 버전 불필요)
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx" }
firebase-dynamic-links-ktx = { group = "com.google.firebase", name = "firebase-dynamic-links-ktx" }
firebase-database-ktx = { group = "com.google.firebase", name = "firebase-database-ktx" }
firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx" }
firebase-storage-ktx = { group = "com.google.firebase", name = "firebase-storage-ktx" }
Expand Down Expand Up @@ -194,7 +193,6 @@ androidx-core-testing = { group = "androidx.arch.core", name = "core-testing", v
[bundles]
firebase = [
"firebase-common-ktx",
"firebase-dynamic-links-ktx",
"firebase-database-ktx",
"firebase-firestore-ktx",
"firebase-storage-ktx",
Expand Down
Loading