> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Exporting wallet keys from mobile apps

Privy's `exportWallet` method requires a secure browser context and therefore can only be executed through the React SDK. Mobile apps built with React Native, Swift, Android, or Flutter can support key export by opening a hosted web page in a WebView. The user logs in on the hosted page and exports their key.

## How it works

1. The user taps "Export key" (or equivalent) in the native app.
2. The app opens the hosted export page URL in a WebView.
3. On the hosted page, the user logs in with Privy (email, OAuth, etc.) if they are not already logged in.
4. After logging in, the user taps "Export wallet" on the page, which calls `exportWallet()` and shows the export modal.
5. When export completes (or fails), the page posts a JSON result back to the native app via a messaging bridge.

## Prerequisites

* Privy integrated into the mobile app with users authenticating and embedded wallets created.
  See the quickstart for [React Native](/basics/react-native/quickstart),
  [Swift](/basics/swift/quickstart), [Android](/basics/android/quickstart), or [Flutter](/basics/flutter/quickstart).
* Familiarity with [exporting wallets](/wallets/wallets/export) via the React SDK.

## 1. Build the export web page

Create a hosted web page where the user can log in with Privy (if needed) and then tap a button to export their wallet key. The page uses the Privy React SDK and posts the result back to the native app via a messaging bridge.

<CodeGroup>
  ```html index.html theme={"system"}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Export wallet</title>
    </head>
    <body>
      <div id="root"></div>
      <script type="module" src="/src/App.tsx"></script>
    </body>
  </html>
  ```

  ```tsx App.tsx theme={"system"}
  import React, {useCallback} from 'react';
  import ReactDOM from 'react-dom/client';
  import {PrivyProvider, usePrivy} from '@privy-io/react-auth';

  function postExportResult(message: object) {
    const json = JSON.stringify(message);
    if (typeof window.ReactNativeWebView !== 'undefined') {
      window.ReactNativeWebView.postMessage(json);
    }
    if (window.webkit?.messageHandlers?.exportResult) {
      window.webkit.messageHandlers.exportResult.postMessage(json);
    }
    if (typeof (window as any).AndroidBridge?.onExportResult === 'function') {
      (window as any).AndroidBridge.onExportResult(json);
    }
    // Flutter WebView (webview_flutter: window.exportResult.postMessage)
    const w = window as any;
    if (typeof w.exportResult?.postMessage === 'function') {
      w.exportResult.postMessage(json);
    }
  }

  function ExportPage() {
    const {ready, authenticated, login, exportWallet} = usePrivy();

    const handleExport = useCallback(() => {
      exportWallet()
        .then(() => postExportResult({status: 'success'}))
        .catch((error) => postExportResult({status: 'error', error: String(error)}));
    }, [exportWallet]);

    if (!ready) return <div>Loading...</div>;

    if (!authenticated) {
      return (
        <div>
          <p>Log in to export your wallet key.</p>
          <button type="button" onClick={() => login()}>
            Log in
          </button>
        </div>
      );
    }

    return (
      <div>
        <p>Export your wallet key to copy it to another wallet.</p>
        <button type="button" onClick={handleExport}>
          Export wallet
        </button>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')!).render(
    <PrivyProvider
      appId="your-privy-app-id"
      config={{
        embeddedWallets: {
          createOnLogin: 'off'
        }
      }}
    >
      <ExportPage />
    </PrivyProvider>
  );
  ```

  ```tsx App.tsx (Solana) theme={"system"}
  import React, {useCallback} from 'react';
  import ReactDOM from 'react-dom/client';
  import {PrivyProvider, usePrivy} from '@privy-io/react-auth';
  import {useExportWallet} from '@privy-io/react-auth/solana';

  function postExportResult(message: object) {
    const json = JSON.stringify(message);
    if (typeof window.ReactNativeWebView !== 'undefined') {
      window.ReactNativeWebView.postMessage(json);
    }
    if (window.webkit?.messageHandlers?.exportResult) {
      window.webkit.messageHandlers.exportResult.postMessage(json);
    }
    if (typeof (window as any).AndroidBridge?.onExportResult === 'function') {
      (window as any).AndroidBridge.onExportResult(json);
    }
    const w = window as any;
    if (typeof w.exportResult?.postMessage === 'function') {
      w.exportResult.postMessage(json);
    }
  }

  function ExportSolanaPage() {
    const {ready, authenticated, login} = usePrivy();
    const {exportWallet} = useExportWallet();

    const handleExport = useCallback(() => {
      exportWallet()
        .then(() => postExportResult({status: 'success'}))
        .catch((error) => postExportResult({status: 'error', error: String(error)}));
    }, [exportWallet]);

    if (!ready) return <div>Loading...</div>;

    if (!authenticated) {
      return (
        <div>
          <p>Log in to export your wallet key.</p>
          <button type="button" onClick={() => login()}>
            Log in
          </button>
        </div>
      );
    }

    return (
      <div>
        <p>Export your Solana wallet key to copy it to another wallet.</p>
        <button type="button" onClick={handleExport}>
          Export Solana wallet
        </button>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')!).render(
    <PrivyProvider appId="your-privy-app-id">
      <ExportSolanaPage />
    </PrivyProvider>
  );
  ```
</CodeGroup>

<Tip>
  Host this page on a domain listed in the app's allowed origins. Configure allowed origins in the
  [Privy Dashboard](https://dashboard.privy.io).
</Tip>

<Warning>
  The hosted page must use the same `appId` as the mobile app so that login and export work
  correctly for your app's users.
</Warning>

## 2. Load the WebView in the native app

When the user taps "Export key", open the hosted export page URL in a WebView. Use incognito or non-persistent storage so that login and export data are not cached.

<Tabs>
  <Tab title="React Native">
    ```tsx theme={"system"}
    import React, {useState, useRef} from 'react';
    import {View, Modal, Button} from 'react-native';
    import {WebView} from 'react-native-webview';

    const EXPORT_PAGE_URL = 'https://your-domain.com/export';

    function ExportWalletScreen() {
      const [visible, setVisible] = useState(false);
      const webViewRef = useRef(null);

      const openExport = () => setVisible(true);

      const handleMessage = (event: any) => {
        const data = JSON.parse(event.nativeEvent.data);
        if (data.status === 'success' || data.status === 'error') {
          setVisible(false);
        }
      };

      return (
        <View>
          <Button title="Export wallet" onPress={openExport} />
          <Modal visible={visible} animationType="slide">
            <WebView
              ref={webViewRef}
              source={{uri: EXPORT_PAGE_URL}}
              onMessage={handleMessage}
              incognito={true}
            />
          </Modal>
        </View>
      );
    }
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={"system"}
    import UIKit
    import WebKit

    class ExportWalletViewController: UIViewController, WKScriptMessageHandler {
        var webView: WKWebView!

        func startExport() {
            let urlString = "https://your-domain.com/export"
            guard let url = URL(string: urlString) else { return }

            let config = WKWebViewConfiguration()
            config.userContentController.add(self, name: "exportResult")
            config.websiteDataStore = .nonPersistent()

            webView = WKWebView(frame: view.bounds, configuration: config)
            view.addSubview(webView)
            webView.load(URLRequest(url: url))
        }

        func userContentController(
            _ userContentController: WKUserContentController,
            didReceive message: WKScriptMessage
        ) {
            guard message.name == "exportResult",
                  let body = message.body as? String else { return }

            // Parse the result and dismiss the WebView
            webView.removeFromSuperview()
            dismiss(animated: true)
        }
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={"system"}
    import android.annotation.SuppressLint
    import android.webkit.JavascriptInterface
    import android.webkit.WebView
    import android.webkit.WebViewClient
    import androidx.appcompat.app.AppCompatActivity

    class ExportWalletActivity : AppCompatActivity() {

        private var webView: WebView? = null

        @SuppressLint("SetJavaScriptEnabled")
        fun startExport() {
            val wv = WebView(this).apply {
                settings.javaScriptEnabled = true
                settings.domStorageEnabled = false
                settings.databaseEnabled = false
                webViewClient = WebViewClient()
                addJavascriptInterface(ExportBridge(), "AndroidBridge")
            }

            wv.clearCache(true)
            wv.clearHistory()
            wv.clearFormData()

            webView = wv
            setContentView(wv)
            wv.loadUrl("https://your-domain.com/export")
        }

        inner class ExportBridge {
            @JavascriptInterface
            fun onExportResult(json: String) {
                runOnUiThread {
                    webView?.clearCache(true)
                    webView?.clearHistory()
                    finish()
                }
            }
        }
    }
    ```
  </Tab>

  <Tab title="Flutter">
    Add `webview_flutter` to your `pubspec.yaml`, then:

    ```dart theme={"system"}
    import 'dart:convert';

    import 'package:flutter/material.dart';
    import 'package:webview_flutter/webview_flutter.dart';

    const String exportPageUrl = 'https://your-domain.com/export';

    class ExportWalletScreen extends StatefulWidget {
      const ExportWalletScreen({super.key});

      @override
      State<ExportWalletScreen> createState() => _ExportWalletScreenState();
    }

    class _ExportWalletScreenState extends State<ExportWalletScreen> {
      bool _visible = false;
      WebViewController? _controller;

      void _openExport() {
        _controller = WebViewController()
          ..setJavaScriptMode(JavaScriptMode.unrestricted)
          ..addJavaScriptChannel(
            'exportResult',
            onMessageReceived: (JavaScriptMessage message) {
              _onExportResult(message.message);
            },
          )
          ..loadRequest(Uri.parse(exportPageUrl));
        setState(() => _visible = true);
      }

      void _onExportResult(String jsonString) {
        final data = jsonDecode(jsonString) as Map<String, dynamic>;
        if (data['status'] == 'success' || data['status'] == 'error') {
          setState(() => _visible = false);
        }
      }

      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            ElevatedButton(
              onPressed: _openExport,
              child: const Text('Export wallet'),
            ),
            if (_visible && _controller != null)
              Expanded(
                child: WebViewWidget(controller: _controller!),
              ),
          ],
        );
      }
    }
    ```

    The hosted page posts to `window.exportResult.postMessage(json)` so the channel name `exportResult` must match.
  </Tab>
</Tabs>

## 3. Handle the export result

When the hosted page completes (or encounters an error), it posts a JSON message back to the native app. Parse this message to determine the outcome and dismiss the WebView.

<Tabs>
  <Tab title="React Native">
    ```tsx theme={"system"}
    const handleMessage = (event: any) => {
      const data = JSON.parse(event.nativeEvent.data);

      if (data.status === 'success') {
        // Key export completed successfully. Dismiss the WebView.
        setVisible(false);
      } else if (data.status === 'error') {
        // An error occurred during export.
        console.error('Export failed:', data.error);
        setVisible(false);
      }
    };
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={"system"}
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        guard message.name == "exportResult",
              let body = message.body as? String,
              let data = body.data(using: .utf8),
              let result = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else { return }

        if result["status"] as? String == "success" {
            // Key export completed successfully. Dismiss the WebView.
        } else {
            // An error occurred during export.
        }

        webView.removeFromSuperview()
        dismiss(animated: true)
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={"system"}
    inner class ExportBridge {
        @JavascriptInterface
        fun onExportResult(json: String) {
            val result = JSONObject(json)

            when (result.getString("status")) {
                "success" -> {
                    // Key export completed successfully. Dismiss the activity.
                }
                "error" -> {
                    // An error occurred during export.
                }
            }

            runOnUiThread { finish() }
        }
    }
    ```
  </Tab>

  <Tab title="Flutter">
    ```dart theme={"system"}
    void _onExportResult(String jsonString) {
      final data = jsonDecode(jsonString) as Map<String, dynamic>;

      if (data['status'] == 'success') {
        // Key export completed successfully. Dismiss the WebView.
        setState(() => _visible = false);
      } else if (data['status'] == 'error') {
        // An error occurred during export.
        debugPrint('Export failed: ${data['error']}');
        setState(() => _visible = false);
      }
    }
    ```
  </Tab>
</Tabs>

## Security considerations

<Info>
  Keep the following in mind when implementing the WebView-based export flow:

  * **Ephemeral WebViews**: Use incognito or non-persistent data store modes (shown in the examples
    above) so that login and export data are not cached on the device.
  * **Separate origin**: The private key is assembled on a different origin from the app and the
    hosted page. Neither the app nor the page can access the full private key.
</Info>
