diff --git a/packages/broadcast-client/src/deep-link.ts b/packages/broadcast-client/src/deep-link.ts new file mode 100644 index 00000000..83a30a2f --- /dev/null +++ b/packages/broadcast-client/src/deep-link.ts @@ -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; +} + +export type DeepLinkHandler = (route: DeepLinkRoute) => void; + +/** + * Deep Link Router class + */ +export class DeepLinkRouter { + private handlers = new Map(); + 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 = {}; + + // 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 = {}; + + 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, + 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(); diff --git a/packages/broadcast-client/src/index.ts b/packages/broadcast-client/src/index.ts index ddee7ead..2824e2b8 100644 --- a/packages/broadcast-client/src/index.ts +++ b/packages/broadcast-client/src/index.ts @@ -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'; + diff --git a/packages/diagnostics-client/src/client.ts b/packages/diagnostics-client/src/client.ts index 0c8611ea..a2ca3f73 100644 --- a/packages/diagnostics-client/src/client.ts +++ b/packages/diagnostics-client/src/client.ts @@ -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?: { diff --git a/packages/diagnostics-client/src/device.ts b/packages/diagnostics-client/src/device.ts index fb8a7371..993cef2e 100644 --- a/packages/diagnostics-client/src/device.ts +++ b/packages/diagnostics-client/src/device.ts @@ -6,6 +6,26 @@ import type { DeviceState } from './types.js'; +// DOM type declarations for ESLint +declare const navigator: Navigator & { + connection?: NetworkInformation; + getBattery?: () => Promise; + 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 diff --git a/packages/diagnostics-client/src/network.ts b/packages/diagnostics-client/src/network.ts index 5bab8488..dfc39564 100644 --- a/packages/diagnostics-client/src/network.ts +++ b/packages/diagnostics-client/src/network.ts @@ -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[][]; + export interface NetworkInterceptorOptions { /** URL patterns to include (default: all) */ includePatterns?: RegExp[]; diff --git a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/DeepLinkRouter.kt b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/DeepLinkRouter.kt new file mode 100644 index 00000000..7b5772b2 --- /dev/null +++ b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/DeepLinkRouter.kt @@ -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 = 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() + 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() + 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() + 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 = 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() diff --git a/packages/swift-platform-sdk/Sources/BLDeepLinkRouter.swift b/packages/swift-platform-sdk/Sources/BLDeepLinkRouter.swift new file mode 100644 index 00000000..31ee8084 --- /dev/null +++ b/packages/swift-platform-sdk/Sources/BLDeepLinkRouter.swift @@ -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") +}