Skip to content

Commit c2ea494

Browse files
authored
fix: 공유 딥링크를 Firebase Dynamic Links에서 App Links로 전환 (#407)
- Firebase Dynamic Links 서비스 종료(2025-08-25)로 깨진 공유 기능을 iOS와 동일한 Universal Link 방식(App Links)으로 교체 - SchemeActivity에서 FirebaseDynamicLinks SDK 대신 intent.data로 직접 쿼리 파싱 - CourseDetailActivity/MyDrawDetailActivity 공유 로직을 SDK 호출 없이 고정 URL(runnect-ios.web.app/share) 조립 방식으로 단순화 - AndroidManifest intent-filter를 rnnt.page.link에서 runnect-ios.web.app/share로 변경, autoVerify 추가 - RunnectDynamicLink → RunnectShareLink로 이름 변경, firebase-dynamic-links-ktx 의존성 제거 Co-authored-by: unam98 <unam98@users.noreply.github.com>
1 parent 1aa8fd6 commit c2ea494

6 files changed

Lines changed: 54 additions & 136 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,15 @@
8686
android:name=".presentation.scheme.SchemeActivity"
8787
android:exported="true">
8888

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

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

9595
<data
96-
android:host="rnnt.page.link"
96+
android:host="runnect-ios.web.app"
97+
android:pathPrefix="/share"
9798
android:scheme="https" />
9899
</intent-filter>
99100

app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailActivity.kt

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.runnect.runnect.presentation.detail
33
import android.app.Activity
44
import android.content.Intent
55
import android.graphics.Rect
6-
import android.net.Uri
76
import android.os.Bundle
87
import android.view.MotionEvent
98
import android.view.View
@@ -13,8 +12,6 @@ import androidx.activity.viewModels
1312
import androidx.core.view.isVisible
1413
import androidx.lifecycle.lifecycleScope
1514
import coil3.load
16-
import com.google.firebase.dynamiclinks.DynamicLink
17-
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
1815
import com.naver.maps.geometry.LatLng
1916
import com.runnect.runnect.R
2017
import com.runnect.runnect.binding.BindingActivity
@@ -50,7 +47,7 @@ import com.runnect.runnect.util.custom.dialog.RequireLoginDialogFragment
5047
import com.runnect.runnect.util.custom.popup.PopupItem
5148
import com.runnect.runnect.util.custom.popup.RunnectPopupMenu
5249
import com.runnect.runnect.util.custom.toast.RunnectToast
53-
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink
50+
import com.runnect.runnect.util.link.RunnectShareLink
5451
import com.runnect.runnect.util.extension.applyScreenEnterAnimation
5552
import com.runnect.runnect.util.extension.applyScreenExitAnimation
5653
import com.runnect.runnect.util.extension.getCompatibleSerializableExtra
@@ -112,7 +109,7 @@ class CourseDetailActivity :
112109
}
113110

114111
private fun initCourseIdExtra() {
115-
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_DYNAMIC_LINK, -1)
112+
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_APP_LINK, -1)
116113
if (idFromLink != -1) {
117114
isFromDeepLink = true
118115
publicCourseId = idFromLink
@@ -230,49 +227,17 @@ class CourseDetailActivity :
230227

231228
private fun initShareButtonClickListener() {
232229
binding.btnShare.setOnClickListener {
233-
sendFirebaseDynamicLink(
234-
title = courseDetail.title,
235-
desc = courseDetail.description,
236-
image = courseDetail.image
237-
)
230+
shareLink("${RunnectShareLink.BASE_URL}?${RunnectShareLink.KEY_PUBLIC_COURSE_ID}=$publicCourseId")
238231
Analytics.logClickedItemEvent(EVENT_CLICK_SHARE)
239232
}
240233
}
241234

242-
private fun sendFirebaseDynamicLink(title: String, desc: String, image: String) {
243-
val link =
244-
"${RunnectDynamicLink.BASE_URL}/?${RunnectDynamicLink.KEY_PUBLIC_COURSE_ID}=$publicCourseId"
245-
246-
FirebaseDynamicLinks.getInstance().createDynamicLink()
247-
.setLink(Uri.parse(link))
248-
.setDomainUriPrefix(RunnectDynamicLink.BASE_URL)
249-
.setAndroidParameters(DynamicLink.AndroidParameters.Builder().build())
250-
.setIosParameters(
251-
DynamicLink.IosParameters.Builder(RunnectDynamicLink.IOS_BUNDLE_ID).build()
252-
)
253-
.setSocialMetaTagParameters(
254-
DynamicLink.SocialMetaTagParameters.Builder()
255-
.setTitle(title)
256-
.setDescription(desc)
257-
.setImageUrl(Uri.parse(image))
258-
.build()
259-
)
260-
.buildShortDynamicLink()
261-
.addOnSuccessListener { result ->
262-
val shortLink = result.shortLink
263-
shareLink(shortLink.toString())
264-
}
265-
.addOnFailureListener {
266-
it.printStackTrace()
267-
}
268-
}
269-
270235
private fun shareLink(url: String) {
271236
val intent = Intent(Intent.ACTION_SEND).apply {
272-
type = RunnectDynamicLink.SEND_INTENT_MIME_TYPE
237+
type = RunnectShareLink.SEND_INTENT_MIME_TYPE
273238
putExtra(Intent.EXTRA_TEXT, url)
274239
}
275-
startActivity(Intent.createChooser(intent, RunnectDynamicLink.INTENT_CHOOSER_TITLE))
240+
startActivity(Intent.createChooser(intent, RunnectShareLink.INTENT_CHOOSER_TITLE))
276241
}
277242

278243
private fun initShowMoreButtonClickListener() {

app/src/main/java/com/runnect/runnect/presentation/scheme/SchemeActivity.kt

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ import android.net.Uri
66
import android.os.Bundle
77
import androidx.activity.enableEdgeToEdge
88
import androidx.appcompat.app.AppCompatActivity
9-
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
109
import com.runnect.runnect.application.PreferenceManager
1110
import com.runnect.runnect.presentation.detail.CourseDetailActivity
1211
import com.runnect.runnect.presentation.login.LoginActivity
1312
import com.runnect.runnect.presentation.storage.mydrawdetail.MyDrawDetailActivity
1413
import com.runnect.runnect.util.analytics.Analytics
1514
import com.runnect.runnect.util.analytics.EventName
1615
import com.runnect.runnect.util.analytics.EventName.Param
17-
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink.KEY_PRIVATE_COURSE_ID
18-
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink.KEY_PUBLIC_COURSE_ID
16+
import com.runnect.runnect.util.link.RunnectShareLink.KEY_PRIVATE_COURSE_ID
17+
import com.runnect.runnect.util.link.RunnectShareLink.KEY_PUBLIC_COURSE_ID
1918
import dagger.hilt.android.AndroidEntryPoint
2019
import timber.log.Timber
2120

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

2827
if (isUserLoggedIn()) {
29-
handleDynamicLinks()
28+
handleAppLink()
3029
} else {
3130
navigateToLoginScreen()
3231
}
@@ -37,54 +36,45 @@ class SchemeActivity : AppCompatActivity() {
3736
return accessToken != "none"
3837
}
3938

40-
private fun handleDynamicLinks() {
41-
FirebaseDynamicLinks.getInstance().getDynamicLink(intent)
42-
.addOnSuccessListener(this) { pendingDynamicLinkData ->
43-
val link = pendingDynamicLinkData.link
44-
45-
if (link != null) {
46-
val publicCourseId = getCourseId(link, KEY_PUBLIC_COURSE_ID)
47-
val privateCourseId = getCourseId(link, KEY_PRIVATE_COURSE_ID)
48-
49-
val targetScreen = when {
50-
publicCourseId != null -> "CourseDetail"
51-
privateCourseId != null -> "MyDrawDetail"
52-
else -> "unknown"
53-
}
54-
Analytics.logEvent(
55-
EventName.SYS_DEEPLINK_OPEN,
56-
Param.DEEPLINK_URL to link.toString(),
57-
Param.TARGET_SCREEN to targetScreen
58-
)
39+
private fun handleAppLink() {
40+
val link = intent.data
41+
if (link == null) {
42+
Timber.e("FAIL: no data in intent")
43+
finish()
44+
return
45+
}
5946

60-
when {
61-
publicCourseId != null -> navigateToCourseDetail<CourseDetailActivity>(
62-
publicCourseId
63-
)
47+
val publicCourseId = getCourseId(link, KEY_PUBLIC_COURSE_ID)
48+
val privateCourseId = getCourseId(link, KEY_PRIVATE_COURSE_ID)
6449

65-
privateCourseId != null -> navigateToCourseDetail<MyDrawDetailActivity>(
66-
privateCourseId
67-
)
50+
val targetScreen = when {
51+
publicCourseId != null -> "CourseDetail"
52+
privateCourseId != null -> "MyDrawDetail"
53+
else -> "unknown"
54+
}
55+
Analytics.logEvent(
56+
EventName.SYS_DEEPLINK_OPEN,
57+
Param.DEEPLINK_URL to link.toString(),
58+
Param.TARGET_SCREEN to targetScreen
59+
)
6860

69-
else -> {
70-
Timber.e("FAIL: could not find course id")
71-
finish()
72-
}
73-
}
74-
}
75-
}
76-
.addOnFailureListener(this) { t ->
77-
Timber.e("FAIL: getDynamicLink from intent : ${t.message}")
61+
when {
62+
publicCourseId != null -> navigateToCourseDetail<CourseDetailActivity>(publicCourseId)
63+
privateCourseId != null -> navigateToCourseDetail<MyDrawDetailActivity>(privateCourseId)
64+
else -> {
65+
Timber.e("FAIL: could not find course id")
66+
finish()
7867
}
68+
}
7969
}
8070

8171
private fun getCourseId(link: Uri, key: String): Int? {
82-
return link.getQueryParameter(key)?.toInt()
72+
return link.getQueryParameter(key)?.toIntOrNull()
8373
}
8474

8575
private inline fun <reified T : Activity> navigateToCourseDetail(courseId: Int) {
8676
Intent(this, T::class.java).apply {
87-
putExtra(EXTRA_FROM_DYNAMIC_LINK, courseId)
77+
putExtra(EXTRA_FROM_APP_LINK, courseId)
8878
startActivity(this)
8979
}
9080
}
@@ -97,7 +87,7 @@ class SchemeActivity : AppCompatActivity() {
9787
}
9888

9989
companion object {
100-
const val EXTRA_FROM_DYNAMIC_LINK = "fromDynamicLink"
90+
const val EXTRA_FROM_APP_LINK = "fromAppLink"
10191
private const val TOKEN_KEY_ACCESS = "access"
10292
}
103-
}
93+
}

app/src/main/java/com/runnect/runnect/presentation/storage/mydrawdetail/MyDrawDetailActivity.kt

Lines changed: 10 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.runnect.runnect.presentation.storage.mydrawdetail
22

33
import android.app.Activity
44
import android.content.Intent
5-
import android.net.Uri
65
import android.os.Bundle
76
import android.widget.TextView
87
import androidx.activity.OnBackPressedCallback
@@ -12,8 +11,6 @@ import androidx.core.view.isVisible
1211
import androidx.core.widget.addTextChangedListener
1312
import com.bumptech.glide.Glide
1413
import com.google.android.material.bottomsheet.BottomSheetDialog
15-
import com.google.firebase.dynamiclinks.DynamicLink
16-
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
1714
import com.naver.maps.geometry.LatLng
1815
import com.runnect.runnect.R
1916
import com.runnect.runnect.binding.BindingActivity
@@ -35,7 +32,7 @@ import com.runnect.runnect.util.custom.toolbar.ToolbarMenu
3532
import com.runnect.runnect.util.analytics.Analytics
3633
import com.runnect.runnect.util.analytics.EventName
3734
import com.runnect.runnect.util.analytics.EventName.Param
38-
import com.runnect.runnect.util.dynamiclink.RunnectDynamicLink
35+
import com.runnect.runnect.util.link.RunnectShareLink
3936
import com.runnect.runnect.util.extension.PermissionUtil
4037
import com.runnect.runnect.util.extension.applyScreenExitAnimation
4138
import com.runnect.runnect.util.extension.hideKeyboard
@@ -44,7 +41,6 @@ import com.runnect.runnect.util.extension.showSnackbar
4441
import com.runnect.runnect.util.extension.showToast
4542
import com.runnect.runnect.util.extension.showWebBrowser
4643
import dagger.hilt.android.AndroidEntryPoint
47-
import timber.log.Timber
4844

4945
@AndroidEntryPoint
5046
class MyDrawDetailActivity :
@@ -56,7 +52,7 @@ class MyDrawDetailActivity :
5652
private val touchList = arrayListOf<LatLng>()
5753
private val selectList = arrayListOf<Int>()
5854
private var courseId = -1
59-
private var isFromDynamicLink = false
55+
private var isFromAppLink = false
6056

6157
override fun onCreate(savedInstanceState: Bundle?) {
6258
super.onCreate(savedInstanceState)
@@ -74,9 +70,9 @@ class MyDrawDetailActivity :
7470
}
7571

7672
private fun initCourseIdExtra() {
77-
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_DYNAMIC_LINK, -1)
73+
val idFromLink = intent.getIntExtra(SchemeActivity.EXTRA_FROM_APP_LINK, -1)
7874
if (idFromLink != -1) {
79-
isFromDynamicLink = true
75+
isFromAppLink = true
8076
courseId = idFromLink
8177
return
8278
}
@@ -283,7 +279,7 @@ class MyDrawDetailActivity :
283279
}
284280

285281
private fun initBackButtonClickListener() {
286-
if (isFromDynamicLink) {
282+
if (isFromAppLink) {
287283
navigateToMainScreen()
288284
return
289285
}
@@ -292,7 +288,7 @@ class MyDrawDetailActivity :
292288
}
293289

294290
private fun addRightMenu() {
295-
if (isFromDynamicLink) {
291+
if (isFromAppLink) {
296292
if (myDrawCourseDetail.isNowUser) {
297293
addShareEditDeleteMenu()
298294
}
@@ -334,43 +330,12 @@ class MyDrawDetailActivity :
334330
}
335331

336332
private fun initShareButtonClickListener() {
337-
createDynamicLink(
338-
title = viewModel.courseTitle,
339-
imgUrl = myDrawCourseDetail.imgUrl
340-
)
341-
}
342-
343-
private fun createDynamicLink(title: String, imgUrl: String) {
344-
val link = "${RunnectDynamicLink.BASE_URL}/?${RunnectDynamicLink.KEY_PRIVATE_COURSE_ID}=${courseId}"
345-
FirebaseDynamicLinks.getInstance().createDynamicLink()
346-
.setLink(Uri.parse(link))
347-
.setDomainUriPrefix(RunnectDynamicLink.BASE_URL)
348-
.setAndroidParameters(DynamicLink.AndroidParameters.Builder().build())
349-
.setIosParameters(
350-
DynamicLink.IosParameters.Builder(RunnectDynamicLink.IOS_BUNDLE_ID).build()
351-
)
352-
.setSocialMetaTagParameters(
353-
DynamicLink.SocialMetaTagParameters.Builder()
354-
.setTitle(title)
355-
.setImageUrl(Uri.parse(imgUrl))
356-
.build()
357-
)
358-
.buildShortDynamicLink()
359-
.addOnSuccessListener { result ->
360-
Timber.d("shortLink: ${result.shortLink}")
361-
shareDynamicLink(result.shortLink.toString())
362-
}
363-
.addOnFailureListener { t ->
364-
Timber.e("sendDynamicLink fail: ${t.message}")
365-
}
366-
}
367-
368-
private fun shareDynamicLink(shortLink: String) {
333+
val url = "${RunnectShareLink.BASE_URL}?${RunnectShareLink.KEY_PRIVATE_COURSE_ID}=$courseId"
369334
val intent = Intent(Intent.ACTION_SEND).apply {
370-
type = RunnectDynamicLink.SEND_INTENT_MIME_TYPE
371-
putExtra(Intent.EXTRA_TEXT, shortLink)
335+
type = RunnectShareLink.SEND_INTENT_MIME_TYPE
336+
putExtra(Intent.EXTRA_TEXT, url)
372337
}
373-
startActivity(Intent.createChooser(intent, RunnectDynamicLink.INTENT_CHOOSER_TITLE))
338+
startActivity(Intent.createChooser(intent, RunnectShareLink.INTENT_CHOOSER_TITLE))
374339
}
375340

376341
private fun createTitleEditBottomSheet(): BottomSheetDialog {

app/src/main/java/com/runnect/runnect/util/dynamiclink/RunnectDynamicLink.kt renamed to app/src/main/java/com/runnect/runnect/util/link/RunnectShareLink.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
package com.runnect.runnect.util.dynamiclink
1+
package com.runnect.runnect.util.link
22

3-
object RunnectDynamicLink {
4-
const val BASE_URL = "https://rnnt.page.link"
5-
const val IOS_BUNDLE_ID = "com.runnect.Runnect-iOS"
3+
object RunnectShareLink {
4+
const val BASE_URL = "https://runnect-ios.web.app/share"
65
const val SEND_INTENT_MIME_TYPE = "text/plain"
76
const val INTENT_CHOOSER_TITLE = "Share Link"
87
const val KEY_PUBLIC_COURSE_ID = "courseId"

gradle/libs.versions.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp"
150150
# Firebase (BOM 관리 - 개별 버전 불필요)
151151
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
152152
firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx" }
153-
firebase-dynamic-links-ktx = { group = "com.google.firebase", name = "firebase-dynamic-links-ktx" }
154153
firebase-database-ktx = { group = "com.google.firebase", name = "firebase-database-ktx" }
155154
firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx" }
156155
firebase-storage-ktx = { group = "com.google.firebase", name = "firebase-storage-ktx" }
@@ -194,7 +193,6 @@ androidx-core-testing = { group = "androidx.arch.core", name = "core-testing", v
194193
[bundles]
195194
firebase = [
196195
"firebase-common-ktx",
197-
"firebase-dynamic-links-ktx",
198196
"firebase-database-ktx",
199197
"firebase-firestore-ktx",
200198
"firebase-storage-ktx",

0 commit comments

Comments
 (0)