feat(sdk): Push deep link routing for all platforms

- TypeScript: DeepLinkRouter with URL parsing and handler registration
- Swift: BLDeepLinkRouter with iOS URL handling and Logger integration
- Kotlin: DeepLinkRouter with Android Uri parsing and handler mapping
- Common screen constants: broadcasts, surveys, settings, profile, etc.
This commit is contained in:
saravanakumardb1 2026-03-03 08:33:56 -08:00
parent 6e0b6c33c9
commit 18dd263797
7 changed files with 562 additions and 0 deletions

View File

@ -0,0 +1,161 @@
/**
* Deep Link Router TypeScript
* Handles routing from push notification deep links to app screens
*/
export interface DeepLinkRoute {
screen: string;
params?: Record<string, string>;
}
export type DeepLinkHandler = (route: DeepLinkRoute) => void;
/**
* Deep Link Router class
*/
export class DeepLinkRouter {
private handlers = new Map<string, DeepLinkHandler>();
private fallbackHandler?: DeepLinkHandler;
/**
* Register a handler for a specific screen
*/
register(screen: string, handler: DeepLinkHandler): void {
this.handlers.set(screen, handler);
}
/**
* Set a fallback handler for unregistered screens
*/
setFallback(handler: DeepLinkHandler): void {
this.fallbackHandler = handler;
}
/**
* Parse a deep link URL and extract route
*/
parseDeepLink(url: string): DeepLinkRoute | null {
try {
const urlObj = new URL(url);
// Handle app-specific URLs: myapp://screen/params
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
const pathParts = urlObj.pathname.split('/').filter(Boolean);
const screen = pathParts[0] || 'home';
const params: Record<string, string> = {};
// Parse query params
urlObj.searchParams.forEach((value, key) => {
params[key] = value;
});
return { screen, params };
}
// Handle web URLs with deep link params
const deepLinkParam = urlObj.searchParams.get('dl');
if (deepLinkParam) {
return this.parseDeepLink(deepLinkParam);
}
// Handle path-based routing: /screen/params
const pathParts = urlObj.pathname.split('/').filter(Boolean);
if (pathParts.length > 0) {
const screen = pathParts[0];
const params: Record<string, string> = {};
urlObj.searchParams.forEach((value, key) => {
params[key] = value;
});
return { screen, params };
}
return null;
} catch {
return null;
}
}
/**
* Handle a deep link route
*/
handle(route: DeepLinkRoute): boolean {
const handler = this.handlers.get(route.screen);
if (handler) {
handler(route);
return true;
}
if (this.fallbackHandler) {
this.fallbackHandler(route);
return true;
}
console.warn(`[DeepLink] No handler for screen: ${route.screen}`);
return false;
}
/**
* Process a deep link URL end-to-end
*/
process(url: string): boolean {
const route = this.parseDeepLink(url);
if (!route) {
console.warn(`[DeepLink] Failed to parse: ${url}`);
return false;
}
return this.handle(route);
}
}
/**
* Create a broadcast deep link URL
*/
export function createBroadcastDeepLink(
baseUrl: string,
screen: string,
params?: Record<string, string>,
broadcastId?: string
): string {
const url = new URL(baseUrl);
url.pathname = `/${screen}`;
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
if (broadcastId) {
url.searchParams.set('broadcastId', broadcastId);
}
return url.toString();
}
/**
* Common deep link screens for broadcast/survey flows
*/
export const DeepLinkScreens = {
// Broadcasts
BROADCAST_DETAIL: 'broadcast',
ANNOUNCEMENTS: 'announcements',
// Surveys
SURVEY: 'survey',
SURVEY_LIST: 'surveys',
// Product-specific (examples)
SETTINGS: 'settings',
PROFILE: 'profile',
UPGRADE: 'upgrade',
SUPPORT: 'support',
// Fallback
HOME: 'home',
} as const;
// Singleton instance for app-wide use
export const deepLinkRouter = new DeepLinkRouter();

View File

@ -169,3 +169,17 @@ export function createUseBroadcast(client: BroadcastClient) {
return { client };
};
}
// =============================================================================
// Deep Link Router
// =============================================================================
export {
DeepLinkRouter,
deepLinkRouter,
DeepLinkScreens,
createBroadcastDeepLink,
type DeepLinkRoute,
type DeepLinkHandler,
} from './deep-link.js';

View File

@ -20,6 +20,16 @@ import { BreadcrumbTrail } from './breadcrumbs.js';
import { NetworkInterceptor } from './network.js';
import { collectDeviceState } from './device.js';
// DOM type declarations for ESLint
type ErrorEvent = {
message: string;
filename: string;
lineno: number;
colno: number;
error?: { stack?: string };
};
type EventListener = (event: unknown) => void;
export interface DiagnosticsClientOptions extends DiagnosticsConfig {
/** Custom logger */
logger?: {

View File

@ -6,6 +6,26 @@
import type { DeviceState } from './types.js';
// DOM type declarations for ESLint
declare const navigator: Navigator & {
connection?: NetworkInformation;
getBattery?: () => Promise<BatteryManager>;
storage?: { estimate(): Promise<{ usage?: number }> };
};
declare const performance: Performance & { memory?: { usedJSHeapSize: number } };
declare const window: Window & {
addEventListener: (type: string, listener: EventListener) => void;
removeEventListener: (type: string, listener: EventListener) => void;
};
type EventListener = (event: { isTrusted: boolean }) => void;
interface NetworkInformation {
effectiveType?: string;
}
interface BatteryManager {
charging: boolean;
level: number;
}
/**
* Collect current device state
* Best-effort: some APIs may not be available in all environments

View File

@ -6,6 +6,10 @@
import type { NetworkRequest } from './types.js';
// DOM type declarations for ESLint
type RequestInfo = string | Request | URL;
type HeadersInit = Headers | Record<string, string> | string[][];
export interface NetworkInterceptorOptions {
/** URL patterns to include (default: all) */
includePatterns?: RegExp[];

View File

@ -0,0 +1,172 @@
package com.bytelyst.platform
import android.net.Uri
import android.util.Log
/**
* Deep Link Route data class
*/
data class DeepLinkRoute(
val screen: String,
val params: Map<String, String> = emptyMap()
)
/**
* Deep link handler type alias
*/
typealias DeepLinkHandler = (DeepLinkRoute) -> Unit
/**
* Deep Link Router class
* Handles routing from push notification deep links to app screens
*/
class DeepLinkRouter {
private val handlers = mutableMapOf<String, DeepLinkHandler>()
private var fallbackHandler: DeepLinkHandler? = null
companion object {
private const val TAG = "DeepLinkRouter"
}
/**
* Register a handler for a specific screen
*/
fun register(screen: String, handler: DeepLinkHandler) {
handlers[screen] = handler
}
/**
* Set a fallback handler for unregistered screens
*/
fun setFallback(handler: DeepLinkHandler) {
fallbackHandler = handler
}
/**
* Parse a deep link URL and extract route
*/
fun parseDeepLink(urlString: String): DeepLinkRoute? {
return try {
val uri = Uri.parse(urlString)
// Handle app-specific URLs: myapp://screen/params
if (uri.scheme != "http" && uri.scheme != "https") {
val pathSegments = uri.pathSegments
val screen = pathSegments.firstOrNull() ?: "home"
val params = mutableMapOf<String, String>()
uri.queryParameterNames.forEach { key ->
uri.getQueryParameter(key)?.let { value ->
params[key] = value
}
}
DeepLinkRoute(screen, params)
}
// Handle web URLs with deep link params
else if (uri.getQueryParameter("dl") != null) {
parseDeepLink(uri.getQueryParameter("dl")!!)
}
// Handle path-based routing: /screen/params
else {
val pathSegments = uri.pathSegments
if (pathSegments.isNotEmpty()) {
val screen = pathSegments[0]
val params = mutableMapOf<String, String>()
uri.queryParameterNames.forEach { key ->
uri.getQueryParameter(key)?.let { value ->
params[key] = value
}
}
DeepLinkRoute(screen, params)
} else {
null
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed to parse deep link: $urlString", e)
null
}
}
/**
* Handle a deep link route
*/
fun handle(route: DeepLinkRoute): Boolean {
val handler = handlers[route.screen]
return if (handler != null) {
handler(route)
true
} else if (fallbackHandler != null) {
fallbackHandler?.invoke(route)
true
} else {
Log.w(TAG, "No handler for screen: ${route.screen}")
false
}
}
/**
* Process a deep link URL end-to-end
*/
fun process(urlString: String): Boolean {
val route = parseDeepLink(urlString)
return if (route != null) {
handle(route)
} else {
Log.w(TAG, "Failed to parse deep link: $urlString")
false
}
}
}
/**
* Create a broadcast deep link URL
*/
fun createBroadcastDeepLink(
baseUrl: String,
screen: String,
params: Map<String, String> = emptyMap(),
broadcastId: String? = null
): String {
val uriBuilder = Uri.parse(baseUrl).buildUpon()
.path("/$screen")
params.forEach { (key, value) ->
uriBuilder.appendQueryParameter(key, value)
}
broadcastId?.let {
uriBuilder.appendQueryParameter("broadcastId", it)
}
return uriBuilder.build().toString()
}
/**
* Common deep link screens
*/
object DeepLinkScreens {
// Broadcasts
const val BROADCAST = "broadcast"
const val ANNOUNCEMENTS = "announcements"
// Surveys
const val SURVEY = "survey"
const val SURVEY_LIST = "surveys"
// Product-specific
const val SETTINGS = "settings"
const val PROFILE = "profile"
const val UPGRADE = "upgrade"
const val SUPPORT = "support"
// Fallback
const val HOME = "home"
}
// Singleton instance for app-wide use
val deepLinkRouter = DeepLinkRouter()

View File

@ -0,0 +1,181 @@
import Foundation
/**
* Deep Link Router Swift
* Handles routing from push notification deep links to app screens
*/
public struct BLDeepLinkRoute {
public let screen: String
public let params: [String: String]
public init(screen: String, params: [String: String] = [:]) {
self.screen = screen
self.params = params
}
}
public typealias BLDeepLinkHandler = (BLDeepLinkRoute) -> Void
/**
* Deep Link Router class
*/
@available(iOS 15.0, *)
public class BLDeepLinkRouter {
private var handlers: [String: BLDeepLinkHandler] = [:]
private var fallbackHandler: BLDeepLinkHandler?
public init() {}
/**
* Register a handler for a specific screen
*/
public func register(screen: String, handler: @escaping BLDeepLinkHandler) {
handlers[screen] = handler
}
/**
* Set a fallback handler for unregistered screens
*/
public func setFallback(handler: @escaping BLDeepLinkHandler) {
fallbackHandler = handler
}
/**
* Parse a deep link URL and extract route
*/
public func parseDeepLink(_ urlString: String) -> BLDeepLinkRoute? {
guard let url = URL(string: urlString) else {
return nil
}
// Handle app-specific URLs: myapp://screen/params
if url.scheme != "http" && url.scheme != "https" {
let pathComponents = url.pathComponents.filter { $0 != "/" && !$0.isEmpty }
let screen = pathComponents.first ?? "home"
var params: [String: String] = [:]
if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems {
for item in queryItems {
if let value = item.value {
params[item.name] = value
}
}
}
return BLDeepLinkRoute(screen: screen, params: params)
}
// Handle web URLs with deep link params
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let dlParam = components.queryItems?.first(where: { $0.name == "dl" })?.value {
return parseDeepLink(dlParam)
}
// Handle path-based routing: /screen/params
let pathComponents = url.pathComponents.filter { $0 != "/" && !$0.isEmpty }
if !pathComponents.isEmpty {
let screen = pathComponents[0]
var params: [String: String] = [:]
if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems {
for item in queryItems {
if let value = item.value {
params[item.name] = value
}
}
}
return BLDeepLinkRoute(screen: screen, params: params)
}
return nil
}
/**
* Handle a deep link route
*/
@discardableResult
public func handle(_ route: BLDeepLinkRoute) -> Bool {
if let handler = handlers[route.screen] {
handler(route)
return true
}
if let fallback = fallbackHandler {
fallback(route)
return true
}
Logger.deepLink.warning("No handler for screen: \(route.screen)")
return false
}
/**
* Process a deep link URL end-to-end
*/
@discardableResult
public func process(_ urlString: String) -> Bool {
guard let route = parseDeepLink(urlString) else {
Logger.deepLink.warning("Failed to parse deep link: \(urlString)")
return false
}
return handle(route)
}
}
/**
* Create a broadcast deep link URL
*/
public func createBroadcastDeepLink(
baseURL: String,
screen: String,
params: [String: String] = [:],
broadcastId: String? = nil
) -> String? {
guard var components = URLComponents(string: baseURL) else {
return nil
}
components.path = "/\(screen)"
var queryItems: [URLQueryItem] = params.map { URLQueryItem(name: $0.key, value: $0.value) }
if let broadcastId = broadcastId {
queryItems.append(URLQueryItem(name: "broadcastId", value: broadcastId))
}
if !queryItems.isEmpty {
components.queryItems = queryItems
}
return components.string
}
/**
* Common deep link screens
*/
public struct BLDeepLinkScreens {
// Broadcasts
public static let broadcast = "broadcast"
public static let announcements = "announcements"
// Surveys
public static let survey = "survey"
public static let surveyList = "surveys"
// Product-specific
public static let settings = "settings"
public static let profile = "profile"
public static let upgrade = "upgrade"
public static let support = "support"
// Fallback
public static let home = "home"
}
// Logger extension
@available(iOS 15.0, *)
extension Logger {
static let deepLink = Logger(subsystem: "com.bytelyst.platform", category: "DeepLink")
}