Thanks a lot. Here is the current minimal Flutter repro code and the exact logs.
Environment
- Flutter app, Android real device
web3auth_flutter 6.3.0
- project on
sapphire_devnet
- redirect URI used by Flutter:
w3a://br.com.wallet.privado_ssi_poc/auth
- MainActivity.onNewIntent implementation (Kotlin)
package br.com.wallet.privado_ssi_poc
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val channelName = “w3a_redirect_bridge”
private var redirectChannel: MethodChannel? = null
private var pendingInitialRedirect: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val dataString = intent?.dataString
val dataUri = intent?.data
Log.d("W3A_NATIVE", "onCreate intent.dataString=$dataString")
Log.d("W3A_NATIVE", "onCreate intent.data=$dataUri")
if (!dataString.isNullOrEmpty()) {
pendingInitialRedirect = dataString
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
redirectChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelName
)
redirectChannel?.setMethodCallHandler { call, result ->
when (call.method) {
"getInitialRedirect" -> {
val initial = pendingInitialRedirect ?: intent?.dataString
pendingInitialRedirect = null
result.success(initial)
}
else -> result.notImplemented()
}
}
pendingInitialRedirect?.let { dispatchRedirect(it) }
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
val dataString = intent.dataString
val dataUri = intent.data
Log.d("W3A_NATIVE", "onNewIntent intent.dataString=$dataString")
Log.d("W3A_NATIVE", "onNewIntent intent.data=$dataUri")
if (!dataString.isNullOrEmpty()) {
dispatchRedirect(dataString)
}
}
private fun dispatchRedirect(uri: String) {
val channel = redirectChannel
if (channel == null) {
pendingInitialRedirect = uri
return
}
Handler(Looper.getMainLooper()).post {
channel.invokeMethod("onRedirect", uri)
}
}
}
- Dart code where initialize() is called + redirect handling
const MethodChannel _redirectBridge = MethodChannel(‘w3a_redirect_bridge’);
Uri _redirectUrl() {
if (Platform.isAndroid) {
return Uri.parse(‘w3a://br.com.wallet.privado_ssi_poc/auth’);
}
return Uri.parse(‘br.com.wallet.privado_ssi_poc://auth’);
}
Future _bindRedirectBridge() async {
_redirectBridge.setMethodCallHandler((MethodCall call) async {
if (call.method == ‘onRedirect’) {
final uri = (call.arguments as String?) ?? ‘’;
await _handleNativeRedirect(uri, fromColdStart: false);
}
});
try {
final initialUri =
await _redirectBridge.invokeMethod(‘getInitialRedirect’);
if (initialUri != null && initialUri.isNotEmpty) {
await _handleNativeRedirect(initialUri, fromColdStart: true);
}
} catch (e, st) {
_logErr(‘_bindRedirectBridge’, e, st);
}
}
Future _initSdk() async {
await Web3AuthFlutter.init(
Web3AuthOptions(
clientId: ‘REDACTED_CLIENT_ID’,
network: Network.sapphire_devnet,
redirectUrl: _redirectUrl(),
),
);
setState(() {
_w3aReady = true;
_status = ‘SDK initialized’;
});
await _attemptRestoreSession();
await _consumePendingRedirectIfAny();
}
Future _attemptRestoreSession() async {
try {
await Web3AuthFlutter.initialize();
await _readCurrentSession(successStatus: ‘Previous session restored’);
} on PlatformException catch (e, st) {
if (_isNoUserFoundError(e)) {
setState(() {
_w3aReady = true;
_w3aLoggedIn = false;
_status = ‘SDK ready — no previous session’;
});
return;
}
_logErr('_attemptRestoreSession', e, st);
setState(() {
_w3aReady = true;
_w3aLoggedIn = false;
_status = 'SDK initialized, but restore failed: $e';
});
}
}
Future _handleNativeRedirect(
String uri, {
required bool fromColdStart,
}) async {
if (uri.isEmpty) return;
_lastRedirectUri = uri;
if (!_w3aReady) {
_pendingRedirectUri = uri;
setState(() {
_status = ‘Redirect received before SDK was ready’;
});
return;
}
_pendingRedirectUri = null;
Web3AuthFlutter.setCustomTabsClosed();
setState(() {
_busy = true;
_status = ‘Processing redirect…’;
});
try {
await Future.delayed(const Duration(milliseconds: 300));
final hasSession = await _pollForPrivKey();
if (hasSession) {
await _readCurrentSession(
successStatus: fromColdStart
? 'Recovered from cold-start redirect '
: 'Recovered after redirect ',
);
} else {
setState(() {
_status = 'Redirect received, but no wallet session was detected.';
});
}
} catch (e, st) {
_logErr(‘_handleNativeRedirect’, e, st);
setState(() => _status = ‘Redirect handling error: $e’);
} finally {
_loginInFlight = false;
setState(() => _busy = false);
}
}
Future _consumePendingRedirectIfAny() async {
final uri = _pendingRedirectUri;
if (uri == null || uri.isEmpty) return;
await _handleNativeRedirect(uri, fromColdStart: true);
}
Future _loginEmailOtp() async {
setState(() {
_busy = true;
_loginInFlight = true;
_status = ‘Starting Email OTP login…’;
});
try {
await Web3AuthFlutter.login(
LoginParams(
loginProvider: Provider.email_passwordless,
mfaLevel: MFALevel.DEFAULT,
extraLoginOptions: ExtraLoginOptions(
login_hint: _emailCtrl.text.trim(),
),
),
);
final hasSession = await _pollForPrivKey();
_loginInFlight = false;
if (hasSession) {
await _readCurrentSession(successStatus: 'Logged in');
} else {
setState(() {
_status = 'Login returned, but no wallet session was detected.';
});
}
} catch (e, st) {
_loginInFlight = false;
_logErr(‘_loginEmailOtp’, e, st);
setState(() => _status = ‘Login error: $e’);
} finally {
if (!_loginInFlight) {
setState(() => _busy = false);
}
}
}
Future pollForPrivKey({int attempts = 16}) async {
for (int i = 0; i < attempts; i++) {
try {
final pk = await Web3AuthFlutter.getPrivKey();
if (pk.isNotEmpty) {
return true;
}
} catch () {}
await Future.delayed(const Duration(milliseconds: 500));
}
return false;
}
Important note:
- I am NOT calling any public Flutter helper like
notifyOnRedirect(...) because I could not find a documented public method like that in the current Flutter SDK API I’m using.
- So currently the deep link is bridged from Android to Dart manually via MethodChannel, and after that I call
setCustomTabsClosed() and poll getPrivKey().
- Logs right after redirect
D/com.web3auth.flutter.web3auth_flutter.Web3AuthFlutterPlugin(…): #login
D/W3A_NATIVE(…): onNewIntent intent.dataString=w3a://br.com.wallet.privado_ssi_poc/auth#b64Params=…
D/W3A_NATIVE(…): onNewIntent intent.data=w3a://br.com.wallet.privado_ssi_poc/auth#b64Params=…
I/flutter (…): [W3A_DIAG] Warm-start redirect received: w3a://br.com.wallet.privado_ssi_poc/auth#b64Params=…
I/flutter (…): [W3A_DIAG] Processing redirect…
D/com.web3auth.flutter.web3auth_flutter.Web3AuthFlutterPlugin(…): #getPrivKey
D/com.web3auth.flutter.web3auth_flutter.Web3AuthFlutterPlugin(…): #getPrivKey
D/com.web3auth.flutter.web3auth_flutter.Web3AuthFlutterPlugin(…): #getPrivKey
…
I/flutter (…): Status = “Redirect received, but no wallet session was detected.”
When I try restore logic with initialize(), I also see:
PlatformException(error, java.util.concurrent.ExecutionException: java.lang.Exception: No user found, please login again!, …)
#0 Web3AuthFlutter.initialize (package:web3auth_flutter/web3auth_flutter.dart:62:7)
Current observed behavior
- OTP page opens
- redirect returns to app
- MainActivity receives the full
w3a://...#b64Params=... callback
- but no active session is surfaced afterward
getPrivKey() stays empty
- no EOA can be derived
Question:
In addition to the previous questions, on current Flutter 6.3.0, is there a required public redirect handoff method that I should be calling after the deep-link return, or is the plugin expected to consume the Android intent automatically?