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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 0.9.12

- Stream native (iOS/Android) logs to the Flutter `DescopeLogger`
- **Breaking Change**: Renamed `DescopeLogger` properties to `level` and `unsafe`

# 0.9.11
- Support links in iOS native flows
- Support `mailto:` links everywhere
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ Descope.setup('<Your-Project-ID>');
Descope.setup('<Your-Project-Id>', (config) {
// set a custom base URL (needs to be set up in the Descope console)
config.baseUrl = 'https://my.app.com';
// enable the logger
if (kDebugMode) {
config.logger = DescopeLogger();
}
// Enable the logger for debugging.
// You can use DescopeLogger.debugLogger (or DescopeLogger.unsafeLogger) during development or to diagnose issues.
// For production it is advised to provide a custom implementation hooked up to the application's log monitoring system.
config.logger = DescopeLogger.debugLogger;
});

// Load any available sessions
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ android {
implementation "androidx.browser:browser:1.8.0"
implementation "androidx.security:security-crypto:1.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
implementation "com.descope:descope-kotlin:0.17.9"
implementation "com.descope:descope-kotlin:0.18.0"
}

testOptions {
Expand Down
67 changes: 67 additions & 0 deletions android/src/main/kotlin/com/descope/flutter/DescopePlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package com.descope.flutter

import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.browser.customtabs.CustomTabsIntent
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import com.descope.Descope
import com.descope.android.DescopeSystemInfo
import com.descope.internal.routes.getPackageOrigin
import com.descope.internal.routes.performAssertion
import com.descope.internal.routes.performNativeAuthorization
import com.descope.internal.routes.performRegister
import com.descope.sdk.DescopeLogger
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
Expand All @@ -25,6 +29,7 @@ import kotlinx.coroutines.launch
/** DescopePlugin */
class DescopePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
private var channel : MethodChannel? = null
private var logChannel: MethodChannel? = null
private var context: Context? = null
private lateinit var storage: Store

Expand Down Expand Up @@ -180,6 +185,38 @@ class DescopePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "descope_flutter/methods")
channel?.setMethodCallHandler(this)

// Set up the log channel with a handler for logger configuration from Flutter
val logsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "descope_flutter/logs")
logChannel = logsChannel
val applicationContext = flutterPluginBinding.applicationContext

logsChannel.setMethodCallHandler { call, result ->
if (call.method == "configure") {
val levelString = call.argument<String>("level")
val unsafe = call.argument<Boolean>("unsafe")

if (levelString == null || unsafe == null) {
result.error("INVALID_ARGS", "Missing level or unsafe arguments", null)
return@setMethodCallHandler
}

val level = when (levelString) {
"error" -> DescopeLogger.Level.Error
"info" -> DescopeLogger.Level.Info
else -> DescopeLogger.Level.Debug
}

// Initialize SDK with the logger configured from Flutter
Descope.setup(applicationContext, projectId = "") {
logger = FlutterDescopeLogger(logsChannel, level, unsafe)
}

result.success(null)
} else {
result.notImplemented()
}
}

flutterPluginBinding.platformViewRegistry.registerViewFactory(
"descope_flutter/descope_flow_view",
DescopeFlowViewFactory(flutterPluginBinding.binaryMessenger)
Expand All @@ -189,6 +226,8 @@ class DescopePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel?.setMethodCallHandler(null)
channel = null
logChannel?.setMethodCallHandler(null)
logChannel = null
}

// ActivityAware
Expand Down Expand Up @@ -272,3 +311,31 @@ private fun createEncryptedStore(context: Context, projectId: String): Store {
}
}
}

// Logger

/**
* A DescopeLogger subclass that forwards all logs to Flutter via a MethodChannel.
* This logger mirrors the level and unsafe settings from the Flutter layer.
*/
private class FlutterDescopeLogger(private val channel: MethodChannel, level: Level, unsafe: Boolean) : DescopeLogger(level, unsafe) {
private val handler = Handler(Looper.getMainLooper())

override fun output(level: Level, message: String, values: List<Any>) {
val levelString = when (level) {
Level.Error -> "error"
Level.Info -> "info"
Level.Debug -> "debug"
}

val valuesArray = values.map { it.toString() }

handler.post {
channel.invokeMethod("log", mapOf(
"level" to levelString,
"message" to message,
"values" to valuesArray,
))
}
}
}
68 changes: 68 additions & 0 deletions ios/Classes/DescopePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,44 @@ public class DescopePlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel = FlutterMethodChannel(name: "descope_flutter/methods", binaryMessenger: registrar.messenger())
let eventChannel = FlutterEventChannel(name: "descope_flutter/events", binaryMessenger: registrar.messenger())
let logChannel = FlutterMethodChannel(name: "descope_flutter/logs", binaryMessenger: registrar.messenger())

let instance = DescopePlugin()

// Set up log channel handler for logger configuration from Flutter
logChannel.setMethodCallHandler { call, result in
if call.method == "configure" {
guard let args = call.arguments as? [String: Any],
let levelString = args["level"] as? String,
let unsafe = args["unsafe"] as? Bool else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing level or unsafe arguments", details: nil))
return
}

let level: DescopeLogger.Level
switch levelString {
case "error":
level = .error
case "info":
level = .info
default:
level = .debug
}

// Initialize SDK with the logger configured from Flutter
// Descope.setup is @MainActor so we ensure we're on the main thread
DispatchQueue.main.async {
Descope.setup(projectId: "") { config in
config.logger = FlutterDescopeLogger(channel: logChannel, level: level, unsafe: unsafe)
}
}

result(nil)
} else {
result(FlutterMethodNotImplemented)
}
}

eventChannel.setStreamHandler(instance)
registrar.addMethodCallDelegate(instance, channel: methodChannel)

Expand Down Expand Up @@ -306,3 +341,36 @@ extension FlutterError {
self.init(code: code, message: message, details: nil)
}
}

/// A DescopeLogger subclass that forwards all logs to Flutter via a MethodChannel.
/// This logger is configured with the level and unsafe settings from the Flutter layer.
private class FlutterDescopeLogger: DescopeLogger {
private let channel: FlutterMethodChannel

init(channel: FlutterMethodChannel, level: Level, unsafe: Bool) {
self.channel = channel
super.init(level: level, unsafe: unsafe)
}

override func output(level: Level, message: String, unsafe values: [Any]) {
let levelString: String
switch level {
case .error:
levelString = "error"
case .info:
levelString = "info"
case .debug:
levelString = "debug"
}

let valuesArray = values.map { String(describing: $0) }

DispatchQueue.main.async {
self.channel.invokeMethod("log", arguments: [
"level": levelString,
"message": message,
"values": valuesArray,
])
}
}
}
20 changes: 20 additions & 0 deletions ios/Classes/descope-swift-sdk/flows/Flow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,23 @@ extension DescopeFlow: CustomStringConvertible {
return "DescopeFlow(url: \"\(url)\")"
}
}

/// Convenience constructor when using Descope's Flow hosting service
extension DescopeFlow {
/// Creates a new ``DescopeFlow`` object that encapsulates a single flow run.
///
/// - Important: This method of creating a ``DescopeFlow`` is only applicable when
/// using Descope's Flow hosting service. If you host your own flows, use the
/// default initializer instead.
///
/// - Parameters:
/// - flowId: The ID of the flow
/// - descope: An optional ``DescopeSDK`` to use instead of the ``Descope`` singleton.
public convenience init(flowId: String, descope: DescopeSDK? = nil) {
let sdk = descope ?? Descope.sdk
precondition(!sdk.config.projectId.isEmpty, "The Descope SDK must be initialized before use")
let url = "\(sdk.client.baseURL)/login/\(sdk.client.config.projectId)?mobile=true&flow=\(flowId)"
self.init(url: url)
self.descope = descope
}
}
31 changes: 24 additions & 7 deletions ios/Classes/descope-swift-sdk/flows/FlowBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,15 @@ extension FlowBridge {
if tag == "fail" {
logger.error("Bridge encountered script error in webpage", message)
} else if logger.isUnsafeEnabled {
logger.debug("Webview console.\(tag): \(message)")
let logMessage = "Webview console.\(tag): \(message)"
switch tag {
case "error":
logger.error(logMessage)
case "warn", "info", "log":
logger.info(logMessage)
default:
logger.debug(logMessage)
}
}
case .found:
logger.info("Bridge received found event")
Expand Down Expand Up @@ -438,12 +446,21 @@ private struct FlowNativeOptions: Encodable {
/// Redirects errors and console logs to the bridge
private let loggingScript = """

window.onerror = (s) => { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'fail', message: s }) }
window.console.error = (s) => { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'error', message: s }) }
window.console.warn = (s) => { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'warn', message: s }) }
window.console.info = (s) => { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'info', message: s }) }
window.console.debug = (s) => { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'debug', message: s }) }
window.console.log = (s) => { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'log', message: s }) }
(function() {
function stringify(args) {
return Array.from(args).map(arg => {
if (!arg) return ""
if (typeof arg === 'string') return arg
return JSON.stringify(arg)
}).join(' ')
}
window.onerror = function() { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'fail', message: stringify(arguments) }) };
window.console.error = function() { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'error', message: stringify(arguments) }) };
window.console.warn = function() { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'warn', message: stringify(arguments) }) };
window.console.info = function() { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'info', message: stringify(arguments) }) };
window.console.debug = function() { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'debug', message: stringify(arguments) }) };
window.console.log = function() { window.webkit.messageHandlers.\(FlowBridgeMessage.log.rawValue).postMessage({ tag: 'log', message: stringify(arguments) }) };
})();

"""

Expand Down
2 changes: 1 addition & 1 deletion ios/Classes/descope-swift-sdk/sdk/SDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public extension DescopeSDK {
static let name = "DescopeKit"

/// The Descope SDK version
static let version = "0.10.4"
static let version = "0.10.5"
}

// Internal
Expand Down
4 changes: 4 additions & 0 deletions lib/src/internal/others/native_log_bridge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Native log bridging is only available on iOS and Android.
// On other platforms, a no-op stub will be used.
export 'native_log_bridge_unsupported.dart' if (dart.library.io) 'native_log_bridge_native.dart';

79 changes: 79 additions & 0 deletions lib/src/internal/others/native_log_bridge_native.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:flutter/services.dart';

import '/src/sdk/config.dart';

/// Bridges log streaming from native SDK layers (iOS/Android) to Flutter.
///
/// This class listens on the `descope_flutter/logs` MethodChannel and forwards
/// received native logs to the Flutter DescopeLogger, which filters based on
/// its own `level` and `unsafe` settings.
class NativeLogBridge {
static const _logChannel = MethodChannel('descope_flutter/logs');
static DescopeLogger? _logger;
static bool _isInitialized = false;

/// Sets the logger to receive native logs and initializes the channel listener.
///
/// This should be called once during SDK initialization. If [logger] is null,
/// native logging will not be initialized on the native side.
static void pipeNativeLogs(DescopeLogger? logger) {
_logger = logger;
_initChannelIfNeeded();

// Send logger configuration to native if a logger is set
if (logger != null) {
final levelString = switch (logger.level) {
DescopeLogger.error => 'error',
DescopeLogger.info => 'info',
_ => 'debug',
};
_logChannel.invokeMethod('configure', {
'level': levelString,
'unsafe': logger.unsafe,
});
}
}

/// Ensures the channel listener is set up.
static void _initChannelIfNeeded() {
if (!_isInitialized) {
_isInitialized = true;
_logChannel.setMethodCallHandler(_handleNativeLog);
}
}

static Future<dynamic> _handleNativeLog(MethodCall call) async {
if (call.method == 'log') {
final logger = _logger;
if (logger == null) return;

final args = call.arguments as Map<dynamic, dynamic>;
final levelString = args['level'] as String;
final message = args['message'] as String;
final values = (args['values'] as List<dynamic>).cast<String>();

// Convert level string to DescopeLogger level constant
final int level;
switch (levelString) {
case 'error':
level = DescopeLogger.error;
break;
case 'info':
level = DescopeLogger.info;
break;
case 'debug':
default:
level = DescopeLogger.debug;
break;
}

// Forward to the Flutter logger, which will filter based on its settings
logger.log(
level: level,
message: message,
values: values,
);
}
}
}

Loading