Skip to content

Commit

Permalink
Merge pull request #4 from MkhytarMkhoian/feature/deeplink
Browse files Browse the repository at this point in the history
Add deeplink support to the app
  • Loading branch information
MkhytarMkhoian authored Jun 3, 2024
2 parents 0f98265 + b41d785 commit 08112fe
Show file tree
Hide file tree
Showing 45 changed files with 786 additions and 83 deletions.
8 changes: 8 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ apply from: "${rootProject.projectDir}/gradle/compose-android-config.gradle"

android {
namespace 'com.moove'

defaultConfig {
buildConfigField "String", "FIREBASE_DYNAMIC_LINK_HOST", "\"$firebase_dynamic_link_host\""
manifestPlaceholders = [
firebaseDynamicLinkHost: "\"$firebase_dynamic_link_host\""
]
}
}

dependencies {
Expand All @@ -26,6 +33,7 @@ dependencies {
implementation libs.androidx.fragment
implementation libs.bundles.androidx.navigation
implementation libs.bundles.androidx.lifecycle
implementation libs.bundles.androidx.compose
// endregion

// region Kotlin
Expand Down
46 changes: 46 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,52 @@
<category android:name="android.intent.category.LAUNCHER" />
</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" />

<!-- Supported schemes -->
<data android:scheme="moove" />

<!-- Corporate subdomains -->
<data android:host="app" />

</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" />

<!-- Supported schemes -->
<data android:scheme="https" />
<data android:scheme="http" />

<!-- Corporate subdomains -->
<data android:host="moove.com" />

<data android:pathPattern="/ticket/confirmation" />
<data android:pathPattern="/home" />
</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" />

<!-- Supported schemes -->
<data android:scheme="https" />
<data android:scheme="http" />

<!-- Corporate subdomains -->
<data android:host="${firebaseDynamicLinkHost}" />

</intent-filter>

<nav-graph android:value="@navigation/app_main_navigation" />
</activity>
</application>
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/moove/app/MooveApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package com.moove.app

import android.app.Application
import com.moove.app.di.coroutineModule
import com.moove.app.di.deepLinkModule
import com.moove.app.di.exceptionsModule
import com.moove.app.di.mainModule
import com.moove.app.di.netModule
import com.moove.app.di.ticketsModule
import com.moove.tickets.di.ticketsModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
Expand All @@ -28,6 +29,7 @@ open class MooveApp : Application() {
exceptionsModule,
ticketsModule,
netModule,
deepLinkModule,
)
}
}
Expand Down
42 changes: 42 additions & 0 deletions app/src/main/java/com/moove/app/di/DeepLinkModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.moove.app.di

import com.google.firebase.Firebase
import com.google.firebase.dynamiclinks.dynamicLinks
import com.moove.BuildConfig
import com.moove.app.feature.deeplink.data.DeeplinkDataRepository
import com.moove.app.feature.deeplink.data.DynamicLinkDataRepository
import com.moove.app.feature.deeplink.data.local.AppDeepLinkLocalDataSource
import com.moove.app.feature.deeplink.data.remote.FirebaseDynamicLinkDataSource
import com.moove.app.feature.deeplink.presentation.DeepLinkAppNavigator
import com.moove.shared.feature.deeplink.domain.DeeplinkRepository
import com.moove.shared.feature.deeplink.domain.DynamicLinkRepository
import com.moove.shared.feature.deeplink.domain.GetDeeplinkUseCase
import com.moove.shared.feature.deeplink.domain.GetDynamicLinkUseCase
import com.moove.shared.feature.deeplink.presentation.DeepLinkNavigator
import org.koin.dsl.module

val deepLinkModule = module {

factory<DeeplinkRepository> { DeeplinkDataRepository(get()) }
factory<DynamicLinkRepository> { DynamicLinkDataRepository(get()) }
factory<DeepLinkNavigator> {
DeepLinkAppNavigator(
ticketsNavigator = get(),
globalAppNavigator = get(),
)
}
factory {
FirebaseDynamicLinkDataSource(
host = BuildConfig.FIREBASE_DYNAMIC_LINK_HOST,
// firebaseDynamicLinks = Firebase.dynamicLinks
)
}
factory { AppDeepLinkLocalDataSource() }
factory {
GetDeeplinkUseCase(
deeplinkRepository = get(),
getDynamicLinkUseCase = get(),
)
}
factory { GetDynamicLinkUseCase(get()) }
}
30 changes: 24 additions & 6 deletions app/src/main/java/com/moove/app/di/MainModule.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
package com.moove.app.di

import android.app.Activity
import com.moove.app.main.AppNavigator
import com.moove.app.feature.home.HomeNavigator
import com.moove.app.feature.home.HomeViewModel
import com.moove.app.main.MainActivityViewModel
import com.moove.app.main.MainNavigator
import com.moove.app.navigation.AppNavigator
import com.moove.shared.navigation.GlobalAppNavigator
import com.moove.shared.navigation.ScreenNavigator
import com.moove.shared.navigation.TicketsNavigator
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.binds
import org.koin.dsl.module

val mainModule = module {

viewModelOf(::MainActivityViewModel)

factory {
AppNavigator(
navController = get(),
coroutineScope = get(),
context = get<Activity>(),
)
} binds arrayOf(
ScreenNavigator::class,
GlobalAppNavigator::class,
TicketsNavigator::class,
)

factory {
HomeNavigator(
navController = get(),
screenNavigator = get(),
)
}
viewModelOf(::HomeViewModel)

viewModelOf(::MainActivityViewModel)
factoryOf(::MainNavigator)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moove.app.feature.deeplink.data

import com.moove.app.feature.deeplink.data.local.AppDeepLinkLocalDataSource
import com.moove.shared.feature.deeplink.domain.DeepLink
import com.moove.shared.feature.deeplink.domain.DeeplinkRepository

class DeeplinkDataRepository(
private val localDataSource: AppDeepLinkLocalDataSource,
) : DeeplinkRepository {

override suspend fun getDeepLink(uri: String): DeepLink {
return localDataSource.getDeepLinkData(uri)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moove.app.feature.deeplink.data

import com.moove.app.feature.deeplink.data.remote.FirebaseDynamicLinkDataSource
import com.moove.shared.feature.deeplink.domain.DynamicLinkRepository

class DynamicLinkDataRepository(
private val dataSource: FirebaseDynamicLinkDataSource,
) : DynamicLinkRepository {

override suspend fun parseLink(uri: String): String? {
return dataSource.parseLink(uri)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.moove.app.feature.deeplink.data.local

import com.moove.app.feature.deeplink.domain.AppDeepLink
import com.moove.core.kotlin.text.matchesPattern
import com.moove.shared.feature.deeplink.domain.DeepLink
import com.moove.tickets.domain.model.Fare
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.StandardCharsets

class AppDeepLinkLocalDataSource(
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {

companion object {
private const val RYDER_ID = "ryderId"
private const val PRICE = "price"

const val HOME = "moove://app/home"
const val FARE_LIST = "moove://app/fare_list"
const val CONFIRM_CONFIRMATION = "/ticket/confirmation"
const val MOOVE_CONFIRM_CONFIRMATION = "moove://app/confirmation"
}

suspend fun getDeepLinkData(uri: String): DeepLink = withContext(backgroundDispatcher) {
when {
uri.matchesPattern(CONFIRM_CONFIRMATION) -> {
val innerUri = URI.create(uri)
val params = getQueryParams(innerUri)
AppDeepLink.Confirmation(
ryderId = params[RYDER_ID]!!,
fare = Fare(
description = "",
price = params[PRICE]?.toFloat()!!
),
)
}

uri.matchesPattern(MOOVE_CONFIRM_CONFIRMATION) -> {
val innerUri = URI.create(uri)
val params = getQueryParams(innerUri)
AppDeepLink.Confirmation(
ryderId = params[RYDER_ID]!!,
fare = Fare(
description = "",
price = params[PRICE]?.toFloat()!!
),
)
}

uri.isThat(FARE_LIST) -> {
val innerUri = URI.create(uri)
val params = getQueryParams(innerUri)
AppDeepLink.FareList(ryderId = params[RYDER_ID]!!)
}

uri.isThat(HOME) || uri.matchesPattern(HOME) -> AppDeepLink.Home
else -> AppDeepLink.Unknown
}
}

private fun String.isThat(type: String): Boolean {
/**
* Handle two cases
* app/profile/ and app/profile
*/
return contains(type, ignoreCase = true)
}

private fun getQueryParams(url: URI): Map<String, String> {
val query = url.query ?: return emptyMap()
return query
.split("&".toRegex())
.filter { it.isNotEmpty() }
.map(::mapQueryParameter)
.associateBy(keySelector = { it.first }, valueTransform = { it.second })
}

private fun mapQueryParameter(query: String): Pair<String, String> {
val index = query.indexOf("=")
val key = if (index > 0) query.substring(0, index) else query
val value = if (index > 0 && query.length > index + 1) {
query.substring(index + 1)
} else null
return Pair(
URLDecoder.decode(key, StandardCharsets.UTF_8.name()),
URLDecoder.decode(value, StandardCharsets.UTF_8.name())
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.moove.app.feature.deeplink.data.remote

import android.net.Uri
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
import com.moove.core.kotlin.text.matchesPattern
import com.moove.shared.feature.deeplink.domain.exceptions.DynamicLinkParseException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext

class FirebaseDynamicLinkDataSource(
private val host: String,
// private val firebaseDynamicLinks: FirebaseDynamicLinks,
private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {

suspend fun parseLink(uri: String): String? = withContext(backgroundDispatcher) {
if (uri.matchesPattern(host).not()) return@withContext null
try {
// firebaseDynamicLinks.getDynamicLink(Uri.parse(uri)).await().link?.toString()
"https://moove.page.link/45hj45j"
} catch (e: Exception) {
throw DynamicLinkParseException(cause = e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.moove.app.feature.deeplink.domain

import com.moove.shared.feature.deeplink.domain.DeepLink
import com.moove.tickets.domain.model.Fare

sealed class AppDeepLink: DeepLink {
data object Unknown : AppDeepLink()
data object Home : AppDeepLink()
data class FareList(val ryderId: String) : AppDeepLink()

data class Confirmation(
val ryderId: String,
val fare: Fare,
) : AppDeepLink()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.moove.app.feature.deeplink.presentation

import com.moove.app.feature.deeplink.domain.AppDeepLink
import com.moove.shared.feature.deeplink.domain.DeepLink
import com.moove.shared.feature.deeplink.presentation.DeepLinkNavigator
import com.moove.shared.navigation.GlobalAppNavigator
import com.moove.shared.navigation.TicketsNavigator

class DeepLinkAppNavigator(
private val globalAppNavigator: GlobalAppNavigator,
private val ticketsNavigator: TicketsNavigator,
) : DeepLinkNavigator {
override fun navigateTo(link: DeepLink) {
when (link) {
is AppDeepLink.FareList -> ticketsNavigator.goFares(link.ryderId)
is AppDeepLink.Confirmation -> {
ticketsNavigator.goFares(link.ryderId)
ticketsNavigator.goToConfirmation(
ryderId = link.ryderId,
fareDescription = link.fare.description,
farePrice = link.fare.price
)
}

is AppDeepLink.Home, AppDeepLink.Unknown -> globalAppNavigator.goHome()
}
}
}
Loading

0 comments on commit 08112fe

Please sign in to comment.