Flutter SDK for embedded wallet

Hi there, everyone! I’m debugging MetaMask Embedded Wallets on Flutter Android and I’m hitting a session recovery issue after a successful Email Passwordless redirect.

Working so far:

1-project on sapphire_devnet

2-Email Passwordless enabled

3-allowlist includes:

br.com.wallet.privado_ssi_poc

w3a://br.com.wallet.privado_ssi_poc

w3a://br.com.wallet.privado_ssi_poc/auth

4-OTP page opens normally

5-after OTP, redirect returns to app

6-MainActivity logs:
onNewIntent intent=w3a://br.com.wallet.privado_ssi_poc/auth#b64Params=…

Problem
After browser return, Web3AuthFlutter.initialize() throws:

PlatformException(… No user found, please login again!)

Also, getPrivKey() stays empty, so no embedded wallet session is detected and no EOA can be derived.

Setup

web3auth_flutter 6.3.0

real Android device

launchMode=“singleTop”

deep link:

scheme: w3a

host: br.com.wallet.privado_ssi_poc

callback definitely reaches app

minSdk = 26

Question
If onNewIntent is receiving the w3a://…#b64Params=… callback correctly, what could still cause initialize() to return No user found, please login again! immediately after redirect on Flutter Android? Please, I would like to request your guidance in this issue. Thank you very much!

Thanks — this is an error report (session recovery) so I looked through the Embedded Wallets / web3auth_flutter docs for likely causes.

Short summary of likely causes and quick checklist to try now

  • Missing hand-off of the deep-link payload to the Flutter/Web3Auth SDK after the browser redirect. The SDK needs the returned URL (the #b64Params payload) to complete session reconstruction.
  • initialize() being called before the redirect is processed (order/timing).
  • onNewIntent not actually setting the Activity intent for Flutter (so Dart never sees the deep link).
  • Fragment/params being lost (some Android browsers/flows can strip # fragments) or not logged/forwarded.
  • Secure storage / key store issues (rare) or a clientId / Sapphire network / connection mismatch that would prevent reconstructing the same key.

Checklist (do these in order)

  1. Verify your Android MainActivity.onNewIntent:

    • Call super.onNewIntent(intent) and setIntent(intent). Example minimal pattern:
      onNewIntent(Intent intent) {
      super.onNewIntent(intent);
      setIntent(intent);
      // log intent.getDataString() to confirm full w3a://…#b64Params=…
      }
    • Log intent.getDataString() and intent.getData() to confirm the fragment (b64Params) is present.
  2. Forward the URL to Web3Auth in Flutter immediately after the redirect:

    • If you use the SDK helper, call the redirect handler (eg. Web3AuthFlutter.notifyOnRedirect(uriString) or the SDK’s documented callback handler) so the SDK can finish session recovery.
    • Make sure this hand-off happens before you call any session-dependent methods (getPrivKey / initialize dependent on recovered session).
  3. Handle both warm and cold starts:

    • For cold start (app killed → opened by deep link) ensure you read the initial intent / initialUri on startup (getInitialUri or equivalent) and call the SDK redirect handler.
    • For warm start use onNewIntent + setIntent as above.
  4. Confirm intent contains the fragment (b64Params). If it’s missing, try a browser that preserves fragments or change the redirect sending approach (or confirm the OAuth flow isn’t stripping it).

  5. Sanity checks:

    • Clear app data and retry (rule out stale/corrupt secure storage).
    • Verify Client ID, Sapphire network (devnet vs mainnet), and connection config match the session you started — mismatches make reconstruction fail.
    • Check web3auth_flutter 6.3.0 release notes / GitHub issues for session recovery bugs.

If this doesn’t fix it, please share (paste):

  • MainActivity.onNewIntent implementation (Java/Kotlin)
  • Dart code where you call Web3AuthFlutter.initialize() and any redirect-handling call (notifyOnRedirect or similar)
  • The logs you see right after redirect (intent log and the initialize() PlatformException stack if available)

I searched the Embedded Wallets / web3auth_flutter docs for redirect & session-handling guidance while preparing this; if you want, paste the two code snippets above and I’ll review them quickly.

Note: This response was generated by an AI bot and may require further verification. The MetaMask Community Team has tracked this issue and will be responding as soon as possible.

Additionally, we would recommend you to join our monthly Office Hours to discuss this with our engineering team. MetaMask Developer Office Hours

1 Like

Thanks a lot for your prompt feedback. The provided clue was important in this matter. I found a possible SDK mismatch and wanted to confirm the expected Flutter behavior.

In my repro:

  • Android callback definitely reaches MainActivity:
    onNewIntent intent=w3a://br.com.wallet.privado_ssi_poc/auth#b64Params=...

  • but after browser return, Web3AuthFlutter.initialize() still throws:
    No user found, please login again!

  • getPrivKey() stays empty, so no EOA can be derived.

What looks suspicious is:

  • older Flutter package docs (web3auth_flutter 3.1.7) showed Android resume handling with Web3AuthFlutter.setResultUrl()

  • the Flutter changelog says setResultUrl was removed in 4.0.0

  • but the current native Android SDK docs still say login tracking requires web3Auth.setResultUrl(intent?.data) on startup and in onNewIntent(intent)

Question:
For current web3auth_flutter 6.3.0 on Android, how is the deep-link payload (#b64Params) supposed to be handed off internally so session recovery can complete?

More specifically:

  • is the Flutter plugin expected to auto-forward the intent data internally, with no public redirect handler needed?

  • or is there currently a required native workaround / MethodChannel bridge for Android deep-link session recovery?

  • do you have a minimal Flutter Android Email Passwordless sample on 6.3.0 that demonstrates successful session reconstruction after redirect?

Once again, thank you very much!

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
  1. 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)
    }
}

}

  1. 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().
  1. 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?

We have passed you case to our team, and we will reply you as soon as we can.

1 Like

Thanks a lot . I really appreciate it. This is important for us because we are developing a high-impact PoC that depends directly on MetaMask Embedded Wallets for the wallet onboarding layer in Flutter Android. Our broader goal is to use your SDK as the entry point for a production-oriented solution, so getting this flow stable is very important for the continuity of the project.

There is no public Dart “redirect handoff” API in web3auth_flutter 6.3.0. On Android the plugin already gets onNewIntent and calls native setResultUrl(intent.data) for you. The old setResultUrl in Dart was removed because that moved into the plugin (the plain Android docs still show MainActivity because they’re for native apps, not Flutter).

Your MethodChannel doesn’t feed Web3Auth — it only runs your Dart logic. The SDK never sees that URI from Dart.

Could you try these:

  1. Don’t call setCustomTabsClosed() on the successful redirect path — when the native “tabs closed” flag is set, the plugin can call setResultUrl(null) and break session completion.

  2. Rely on await Web3AuthFlutter.login(...) to finish after OTP (it should complete when the native layer processes the deep link), instead of polling getPrivKey() after your own channel callback.

initialize() is for restoring an existing session, not for consuming the OAuth return; “no user” on first launch is normal.

You can keep MainActivity with super.onNewIntent + setIntent(intent); you don’t need a MethodChannel for Web3Auth itself.

1 Like

Hi there! Thanks a lot for your prompt feedback! I really appreciate it. Quick update after applying the changes you suggested.

We removed the custom Dart redirect bridge and are now using the simplified flow only:

Web3AuthFlutter.init(…)

initialize() only for restoring a previous session

await Web3AuthFlutter.login(…)

then getPrivKey() / derive EOA

MainActivity only does super.onNewIntent(intent) + setIntent(intent) and logs the URI

we are not calling setCustomTabsClosed() on the successful redirect path

The issue still remains.

Observed behavior:

-OTP page opens

-redirect returns to the app

-MainActivity receives the callback correctly

-but afterward no wallet session is surfaced

-getPrivKey() is still empty

-so no EOA can be derived

Relevant 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=…

At this point, the callback is definitely reaching MainActivity, but the authenticated session still does not seem to be materialized in Flutter Android afterward.

Could you please advise on the next step from here?
More specifically:

  1. Is there any known issue in current web3auth_flutter 6.3.0 where login() returns from redirect but the wallet session is not finalized on Android?

  2. Is there any extra Android-side requirement beyond super.onNewIntent(intent) + setIntent(intent) for the plugin to complete session reconstruction?

3)Would you like us to test a specific provider such as Google in the same minimal repro to compare behavior?

Onece again, thank you very much for your guidance.

By the way, I would like to share my MainActivity.kt after the updates you kindly suggested:

package br.com.wallet.privado_ssi_poc

import android.content.Intent
import android.os.Bundle
import android.util.Log
import io.flutter.embedding.android.FlutterActivity

class MainActivity : FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(
            "W3A_NATIVE",
            "onCreate dataString=${intent?.dataString} data=${intent?.data}"
        )
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        Log.d(
            "W3A_NATIVE",
            "onNewIntent dataString=${intent.dataString} data=${intent.data}"
        )
    }
}

Here’s another relevant logs, in addition to the the previous ones:

Relevant logs

D/com.web3auth.flutter.web3auth_flutter.Web3AuthFlutterPlugin(28624): #login

D/W3A_NATIVE(28624): onNewIntent dataString=w3a://br.com.wallet.privado_ssi_poc/auth#b64Params=... data=w3a://br.com.wallet.privado_ssi_poc/auth#b64Params=...

I/flutter (28624): [W3A_BROWSER_DIAG] Lifecycle changed: AppLifecycleState.resumed

I/flutter (28624): [W3A_BROWSER_DIAG] _loginEmailOtp.timeout: TimeoutException after 0:03:00.000000: Future not completed

Thanks for sharing the details.

We tested the flow using the example project included with the SDK, and it’s working as expected. The OTP flow completes, callback is received, and the wallet session is properly established (able to fetch getPrivKey()). We’ve also attached a screenshot for reference.

1 Like

Hi, there

Thanks a lot for your feedback! I really appreciate it. Please, would you mind sharing your code snippet and environment configs in order to use them as a guidance? It will be very helpful.

Once again, thank you very much!

In addition to my previous reply, I would like to point out that we found the official repos on the Web3Auth GitHub org. It looks like there is a dedicated Flutter repo called web3auth-flutter-examples. Can you confirm whether that is the exact example project your team used for the successful OTP/session test? If yes, we’ll run that repo unchanged first and compare it directly with our current app.

Once again, thank you very much!

Hi there,

Folks, please, I would like to know the following: are there any updates regarding my previous replies ?Thank yo very much!

Example attached with sdk.

1 Like

Hi there,

Thanks a lot! I really appreciate your feedback. I am reviewing the example attached to the sdk repository so I can successfully adapt it to my use case. In the meantime, I would like to ask: since external wallets are enabled in the dashboard and metamask is enabled there by default, how is that flow expected to be triggered from web3auth_flutter? Is it available through a Flutter API other than Web3AuthFlutter.login(LoginParams(…)), or is it only accessible through a modal/UI flow that is not represented by a Provider enum?

Once again, thank you very much for all your guidance!

Unfortunately the external wallets won’t work with flutter. External wallets aggregator only work on the web version of web3auth.

1 Like

Hi there,

Thanks a lot for the clarification regarding external wallets on Flutter. That helps a lot.

We have now isolated a separate issue that seems unrelated to the external-wallet flow, and I would really appreciate your guidance on it.

We created a minimal Flutter repro based directly on the example attached to the SDK repository, and replaced the login path with our custom JWT flow. The current behavior is:

1-Web3Auth initializes successfully
2-We perform a mock Web2 login against our backend
3-The backend returns a valid custom JWT
4-We call Web3AuthFlutter.login(LoginParams(loginProvider: Provider.jwt, …))
5-The hosted page opens normally
6-The redirect returns to the app successfully
7-MainActivity.onNewIntent() receives the callback URI correctly
8-However, the Flutter login future never resolves, getPrivKey() remains empty, and no wallet session is surfaced

Relevant callback log:

D/W3A_NATIVE: onNewIntent intent.dataString=w3a://br.com.wallet.privado_ssi_poc/auth#state=jwt-login&b64Params=…
D/W3A_NATIVE: onNewIntent intent.data=w3a://br.com.wallet.privado_ssi_poc/auth#state=jwt-login&b64Params=…

We also validated the JWT side carefully:

JWT is compact and well-formed
JWKS endpoint is publicly reachable
iss, aud, sub, and kid match the dashboard configuration
manifest deep link is working, since the callback reaches the app

So at this point, it looks like the redirect is reaching Android correctly, but the embedded-wallet session is not being materialized afterward in Flutter.

Would you be able to confirm whether this is a known issue with Provider.jwt on Flutter Android, or whether there is any additional required step for the plugin to surface the session after onNewIntent() receives the callback?

By the way, in case you folks have another example available, it will be very helpful.

Thanks again for all your support.

Hi everyone,

In addition to my previous reply, I would like to point out that we were able to successfully implement passwordless login by following the Flutter sdk example. In that scenario, the example was useful and I could successfully adapt it to my flow.

However, the same was not possible for our JWT login flow. As mentioned before, unfortunately, we are still facing the same post-redirect behavior: the Web3Auth page opens, the redirect returns to the app, MainActivity.onNewIntent receives the callback correctly, but no wallet session is surfaced afterward and getPrivKey() still returns no user/session.

There is one important distinction in our current issue I would like to add: for the JWT flow, we are not using Auth / Auth0-based authentication. We are using Custom Authentication (Custom JWT connection) configured in the dashboard.

Because of that, the Flutter SDK example attached to the repository is not very helpful for this specific issue. That example is centered around the Auth flow, while our JWT integration uses:

a- a Custom JWT connection
b- our own backend-issued JWT
c- our own JWKS endpoint
d- Provider.jwt in Flutter

At this point and as mentioned before, we have already validated that:

  • the callback reaches onNewIntent
  • the JWT is valid
  • the JWT verifies successfully against our remote JWKS
  • the Custom JWT connection values (iss, aud, kid, Auth Connection ID) are aligned

So the remaining issue appears to be in the Flutter Android post-redirect session finalization path for custom authentication with JWT, not in the JWT generation itself.

For that reason, the SDK example based on Auth does not help us isolate this case. We would really appreciate guidance or a working Flutter sample specifically for Custom Authentication / Custom JWT on Android, especially covering the post-redirect session establishment path.

Thank you very much for your support! I really appreciate it!

Hi there,

How’ve you been @yashovardhan and @Gaurav_Goel ? Please, let me know if you folks need anything else in order to assist me in my last issue. I look forward to your reply!

Thank you very much for your support!