I'm encountering an issue with Jetpack Compose deeplinks where they're consistently redirecting to the ResetPasswordRoute destination, despite having different uriPattern configurations. Here's an overview of my setup:
Manifest Configuration:
<activity
android:name=".activity.MainActivity"
android:exported="true"
android:theme="@style/Theme.ServiceSpotter">
<!-- Intent filter for email verification -->
<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:scheme="https" android:host="handypro.pro" />
</intent-filter>
<!-- Intent filter for password reset -->
<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:scheme="https" android:host="staging.handypro.pro" />
</intent-filter>
</activity>
Navigation Setup:
@Composable
fun RootNavigation(
isLoggedIn: Boolean,
isWalkthroughCompleted: Boolean,
navController: NavHostController,
isSystemInDarkTheme: Boolean
) {
NavHost(
navController = navController,
startDestination = SplashScreenDestination.route
) {
splashNavGraph(
isLoggedIn,
isWalkthroughCompleted,
onNavigateToHome = {
navController.navigate(MainScreenDestination.destination) {
popUpTo(SplashScreenDestination.route) {
inclusive = true
}
}
},
onNavigateToLogin = {
navController.navigate(AuthDestination.route) {
popUpTo(SplashScreenDestination.route) {
inclusive = true
}
}
},
onNavigateToWalkthrough = {
navController.navigate(OnBoardingDestination.route) {
popUpTo(SplashScreenDestination.route) {
inclusive = true
}
}
}
)
walkthroughNavGraph(
onSignInClicked = {
navController.navigate(AuthDestination.route)
},
onGoogleSignInClicked = {},
onGetStartedClicked = {
navController.navigate(RegisterDestination.route)
}
)
authNavGraph(
onNavigateToHome = {
navController.navigate(MainScreenDestination.destination) {
popUpTo(SplashScreenDestination.route) {
inclusive = true
}
}
},
onNavigateToOtp = {},
onGoogleSignInClicked = {},
onBackClicked = {
navController.popBackStack()
},
onRegisterBackClicked = {
navController.popBackStack()
},
onCreateAccountClicked = {
navController.navigate(RegisterDestination.route)
},
onForgotPasswordClicked = {
navController.navigate(ForgotPasswordDestination.route)
},
isSystemInDarkMode = isSystemInDarkTheme,
)
mainScreenNavGraph(isSystemInDarkTheme)
}
}
Auth Nav Graph
fun NavGraphBuilder.authNavGraph(
onNavigateToHome: () -> Unit = {},
onForgotPasswordClicked: () -> Unit = {},
onGoogleSignInClicked: () -> Unit = {},
onCreateAccountClicked: () -> Unit = {},
onBackClicked: () -> Unit = {},
onRegisterBackClicked: () -> Unit = {},
onNavigateToOtp: () -> Unit = {},
isSystemInDarkMode: Boolean = false,
) {
navigation(
startDestination = LoginDestination.destination,
route = AuthDestination.route
) {
composable(route = LoginDestination.destination,
enterTransition = { slideIn },
exitTransition = { slideOut }) {
LoginRoute(
onNavigateHome = onNavigateToHome,
onForgotPasswordClicked = onForgotPasswordClicked,
onGoogleSignInClicked = onGoogleSignInClicked,
onCreateAccountClicked = onCreateAccountClicked,
onBackClicked = onBackClicked
)
}
composable(
route = RegisterDestination.route,
enterTransition = { slideIn },
exitTransition = { slideOut }
) {
RegisterRoute(
onNavigateToOtp = onNavigateToOtp,
onBackClick = onRegisterBackClicked
)
}
composable(
route = ForgotPasswordDestination.route,
enterTransition = { slideIn },
exitTransition = { slideOut }
) {
ForgotPasswordRoute(
isSystemInDarkMode = isSystemInDarkMode,
onBackClick = onBackClicked,
)
}
composable(
route = VerificationDestination.route,
deepLinks = listOf(
navDeepLink {
uriPattern = BuildConfig.BASE_URL + "email/verify/{id}?expires={expires}&hash={hash}&signature={signature}"
action = Intent.ACTION_VIEW
}
),
arguments = listOf(
navArgument("id") {
type = StringType
defaultValue = ""
},
navArgument("expires") {
type = StringType
defaultValue = ""
},
navArgument("hash") {
type = StringType
defaultValue = ""
},
navArgument("signature") {
type = StringType
defaultValue = ""
}
),
enterTransition = { slideIn },
exitTransition = { slideOut },
) { backStackEntry ->
val context = LocalContext.current
val intent = remember(backStackEntry) {
context.findActivity()?.intent
}
val uri = intent?.data
val id = uri?.pathSegments?.get(3) ?: ""
val expires = uri?.getQueryParameter("expires") ?: ""
val hash = uri?.getQueryParameter("hash") ?: ""
val signature = uri?.getQueryParameter("signature") ?: ""
VerificationRoute(
isSystemInDarkMode = isSystemInDarkMode,
id = id,
expires = expires,
hash = hash,
signature = signature
)
}
composable(
route = ResetPasswordDestination.route,
deepLinks = listOf(
navDeepLink {
uriPattern = BuildConfig.BASE_URL + "password/email?token={token}&email={email}"
action = Intent.ACTION_DEFAULT
}
),
arguments = listOf(
navArgument("token") {
type = StringType
defaultValue = ""
},
navArgument("email") {
type = StringType
defaultValue = ""
}
),
enterTransition = { slideIn },
exitTransition = { slideOut }
) { backStackEntry ->
val context = LocalContext.current
val intent = remember(backStackEntry) {
context.findActivity()?.intent
}
val uri = intent?.data
val token = uri?.getQueryParameter("token") ?: ""
val email = uri?.getQueryParameter("email") ?: ""
ResetPasswordRoute(
isSystemInDarkMode = false,
onBackClick = onBackClicked,
token = token,
email = email
)
}
}
}
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
MainActivity Setup
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel by viewModels<MainVewModel>()
val notificationLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (
isAtLeastApi(Build.VERSION_CODES.TIRAMISU) &&
hasPermission(Manifest.permission.POST_NOTIFICATIONS).not()
) {
notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
setContent {
val isLoggedIn by viewModel.isLoggedIn.collectAsStateWithLifecycle()
val isWalkthroughCompleted by viewModel.walkthroughCompleted.collectAsStateWithLifecycle()
ServiceSpotterTheme {
val navController = rememberNavController()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
RootNavigation(
isLoggedIn = isLoggedIn,
navController = navController,
isWalkthroughCompleted = isWalkthroughCompleted,
isSystemInDarkTheme = isSystemInDarkTheme()
)
}
}
}
}
}
Issue Description:
Despite having different uriPattern configurations for email verification and password reset, Jetpack Compose deeplinks consistently redirect to the ResetPasswordRoute destination, regardless of the URL clicked. The BASE_URL in BuildConfig is "https://staging.handypro.pro/api/", but deeplinks from both https://handypro.pro and https://staging.handypro.pro domains redirect to the same destination.
Expected Behavior:
I expect deeplinks to correctly navigate to the appropriate destinations based on their uriPattern configurations, directing to either email verification or password reset routes accordingly.
Additional Context:
- I've verified that the deeplink URLs match the defined uriPattern configurations.
- The domain `handypro.pro` and `staging.handypro.pro` have been verified in the server via `assetlinks.json`
- The issue persists across different devices and environments.
- Any insights or suggestions on debugging steps or potential solutions would be greatly appreciated.
- In addition I tested the links both using emulator and physical devices and also used android studio App Link Assistant to test but all result to same behavior
Here are the screenshots for the screens
[![ResetPasswordRoute Screenshot][1]][1] [![Verify Email Link][2]][2] [![Reset Password Link][3]][3]
| ResetPasswordRoute | Verify Email Link | Reset Password Link |
|---|---|---|
![]() |
![]() |
![]() |


