Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ jobs:
sparkling_method:
- 'packages/sparkling-method/**'
sparkling_router:
- 'packages/methods/sparkling-router/**'
- 'packages/sparkling-router/**'
- 'packages/sparkling-router-plugin/**'
sparkling_storage:
- 'packages/methods/sparkling-storage/**'
sparkling_sdk:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class RouterCloseMethod : AbsRouterCloseMethodIDL() {
}

val containerID = params.containerID
val animated = params.animated ?: true // Default to animated close
val animated = params.animated ?: false

val success =
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@ class RouterOpenMethod : AbsRouterOpenMethodIDL() {

val replace = params.replace ?: false
val useSysBrowser = params.useSysBrowser ?: false
val animated = params.animated ?: false
val extra = params.extra

val extraInfo =
mutableMapOf<String, Any>(
"useSysBrowser" to useSysBrowser,
"animated" to animated,
"interceptor" to (params.interceptor ?: ""),
"extra" to (extra ?: emptyMap<Any, Any>()),
)

Expand All @@ -103,20 +106,20 @@ class RouterOpenMethod : AbsRouterOpenMethodIDL() {
try {
when (replaceType) {
ReplaceType.alwaysCloseBeforeOpen -> {
routerDepend.closeView(getSDKContext(), type)
routerDepend.closeView(getSDKContext(), type, animated = animated)
routerDepend.openScheme(getSDKContext(), scheme, extraInfo, type, context = context)
}

ReplaceType.alwaysCloseAfterOpen -> {
val opened = routerDepend.openScheme(getSDKContext(), scheme, extraInfo, type, context = context)
routerDepend.closeView(getSDKContext(), type)
routerDepend.closeView(getSDKContext(), type, animated = animated)
opened
}

ReplaceType.onlyCloseAfterOpenSucceed -> {
val opened = routerDepend.openScheme(getSDKContext(), scheme, extraInfo, type, context = context)
if (opened) {
routerDepend.closeView(getSDKContext(), type)
routerDepend.closeView(getSDKContext(), type, animated = animated)
}
opened
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2026 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
package com.tiktok.sparkling.method.router.stack

import com.tiktok.sparkling.method.registry.core.annotation.IDLMethodName
import com.tiktok.sparkling.method.registry.core.annotation.IDLMethodParamField
import com.tiktok.sparkling.method.registry.core.annotation.IDLMethodParamModel
import com.tiktok.sparkling.method.registry.core.annotation.IDLMethodResultModel
import com.tiktok.sparkling.method.registry.core.base.AbsSparklingIDLMethod
import com.tiktok.sparkling.method.registry.core.model.idl.IDLMethodBaseParamModel
import com.tiktok.sparkling.method.registry.core.model.idl.IDLMethodBaseResultModel

abstract class AbsRouterStackMethodIDL :
AbsSparklingIDLMethod<
AbsRouterStackMethodIDL.IDLMethodStackParamModel,
AbsRouterStackMethodIDL.IDLMethodStackResultModel,
>() {
@IDLMethodName(
name = "router.stack",
params = [
"command",
"path",
"search",
"bundle",
"scheme",
"presentation",
"entryId",
"entries",
"result",
"animated",
"usePrefetched",
],
results = ["entryId", "state"],
)
final override val name: String = "router.stack"

@IDLMethodParamModel
interface IDLMethodStackParamModel : IDLMethodBaseParamModel {
@get:IDLMethodParamField(required = true, isGetter = true, keyPath = "command")
val command: String

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "path")
val path: String?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "search")
val search: Map<String, Any>?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "bundle")
val bundle: String?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "scheme")
val scheme: String?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "presentation")
val presentation: String?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "entryId")
val entryId: String?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "entries")
val entries: List<Map<String, Any>>?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "result")
val result: Any?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "animated")
val animated: Boolean?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "usePrefetched")
val usePrefetched: Boolean?
}

@IDLMethodResultModel
interface IDLMethodStackResultModel : IDLMethodBaseResultModel {
@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "entryId")
@set:IDLMethodParamField(required = false, isGetter = false, keyPath = "entryId")
var entryId: String?

@get:IDLMethodParamField(required = false, isGetter = true, keyPath = "state")
@set:IDLMethodParamField(required = false, isGetter = false, keyPath = "state")
var state: Map<String, Any>?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) 2026 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
package com.tiktok.sparkling.method.router.stack

import com.tiktok.sparkling.method.registry.core.BridgePlatformType
import com.tiktok.sparkling.method.registry.core.IDLBridgeMethod
import com.tiktok.sparkling.method.registry.core.model.idl.CompletionBlock
import com.tiktok.sparkling.method.registry.core.utils.createXModel
import com.tiktok.sparkling.method.router.utils.RouterProvider
import com.tiktok.sparkling.method.router.utils.RouterStackCommand
import com.tiktok.sparkling.method.router.utils.RouterStackTarget

class RouterStackMethod : AbsRouterStackMethodIDL() {
override fun handle(
params: IDLMethodStackParamModel,
callback: CompletionBlock<IDLMethodStackResultModel>,
type: BridgePlatformType,
) {
val commandName =
params.command.ifBlank {
callback.onFailure(IDLBridgeMethod.INVALID_PARAM, "command must not be empty")
return
}
val routerDepend =
RouterProvider.hostRouterDepend ?: run {
callback.onFailure(IDLBridgeMethod.FAIL, "Router service not available")
return
}
val entries = mutableListOf<RouterStackTarget>()
if (commandName == "reset") {
params.entries.orEmpty().forEachIndexed { index, entry ->
val target = targetFrom(entry)
if (target == null) {
callback.onFailure(
IDLBridgeMethod.INVALID_PARAM,
"entries[$index] requires scheme, path, and a valid presentation",
)
return
}
entries += target
}
if (entries.isEmpty()) {
callback.onFailure(IDLBridgeMethod.INVALID_PARAM, "reset requires entries")
return
}
}
val target = targetFrom(params)
if (commandName in setOf("push", "replace", "prefetch", "syncOwnLocation") && target == null) {
callback.onFailure(IDLBridgeMethod.INVALID_PARAM, "$commandName requires a valid target")
return
}
if (commandName == "popTo" && params.entryId.isNullOrBlank()) {
callback.onFailure(IDLBridgeMethod.INVALID_PARAM, "popTo requires entryId")
return
}
val command =
RouterStackCommand(
command = commandName,
target = target,
entryId = params.entryId,
entries = entries,
result = params.result,
animated = params.animated ?: true,
usePrefetched = params.usePrefetched ?: false,
)
val result =
try {
routerDepend.executeStackCommand(
getSDKContext(),
command,
getSDKContext()?.context,
)
} catch (error: Throwable) {
callback.onFailure(
IDLBridgeMethod.FAIL,
"Stack command failed: ${error.message ?: error::class.java.simpleName}",
)
return
}

if (result == null) {
callback.onFailure(IDLBridgeMethod.FAIL, "Stack protocol is not implemented by host")
return
}
if (!result.success) {
callback.onFailure(IDLBridgeMethod.FAIL, result.message)
return
}
callback.onSuccess(
IDLMethodStackResultModel::class.java.createXModel(
getSDKContext()?.containerID,
).apply {
entryId = result.entryId
state = result.state
},
)
}

private fun targetFrom(params: IDLMethodStackParamModel): RouterStackTarget? {
val path = params.path?.takeIf { it.isNotBlank() } ?: return null
val scheme =
params.scheme?.takeIf { it.isNotBlank() }
?: if (params.command == "syncOwnLocation") "" else return null
val presentation = params.presentation ?: "push"
if (presentation !in setOf("push", "modal")) return null
return RouterStackTarget(
path = path,
search = params.search.orEmpty().mapValues { it.value.toString() },
bundle = params.bundle.orEmpty(),
scheme = scheme,
presentation = presentation,
)
}

private fun targetFrom(value: Map<String, Any>): RouterStackTarget? {
val scheme = value["scheme"]?.toString()?.takeIf { it.isNotBlank() } ?: return null
val path = value["path"]?.toString()?.takeIf { it.isNotBlank() } ?: return null
val search =
(value["search"] as? Map<*, *>)
.orEmpty()
.mapNotNull { (key, item) ->
key?.toString()?.let { it to item.toString() }
}.toMap()
val presentation = value["presentation"]?.toString() ?: "push"
if (presentation !in setOf("push", "modal")) return null
return RouterStackTarget(
path = path,
search = search,
bundle = value["bundle"]?.toString().orEmpty(),
scheme = scheme,
presentation = presentation,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ interface IHostRouterDepend {
animated: Boolean? = false,
): Boolean

fun executeStackCommand(
bridgeContext: IBridgeContext?,
command: RouterStackCommand,
context: Context?,
): RouterStackResult? = null

fun provideRouteOpenHandlerList(contextProviderFactory: ContextProviderFactory?): List<AbsRouteOpenHandler> = listOf()

fun provideRouteOpenExceptionHandler(contextProviderFactory: ContextProviderFactory?): AbsRouteOpenHandler? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2026 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
package com.tiktok.sparkling.method.router.utils

data class RouterStackTarget(
val path: String,
val search: Map<String, String>,
val bundle: String,
val scheme: String,
val presentation: String,
)

data class RouterStackCommand(
val command: String,
val target: RouterStackTarget?,
val entryId: String?,
val entries: List<RouterStackTarget>,
val result: Any?,
val animated: Boolean,
val usePrefetched: Boolean,
)

data class RouterStackResult(
val success: Boolean,
val message: String,
val entryId: String? = null,
val state: Map<String, Any>? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,9 @@ class RouterCoverageTest {

assertEquals(IDLBridgeMethod.FAIL, callback.failureCode)
assertEquals("Failed to close current container", callback.failureMsg)
// animated default is true when null
// animated defaults to false on every platform
verify(exactly = 1) {
hostRouter.closeView(bridgeContext, BridgePlatformType.LYNX, " ", true)
hostRouter.closeView(bridgeContext, BridgePlatformType.LYNX, " ", false)
}
}

Expand Down
Loading
Loading