From 0d5196b7190f00a07f8e8ac66c3bc873e1c245ea Mon Sep 17 00:00:00 2001 From: Andrey Dobrikov Date: Mon, 23 Mar 2026 13:56:42 -0400 Subject: [PATCH 01/13] Added Windows Support Windows native WebView plugin Message passing between web view and Unity --- .gitignore | 11 + SDK/Plugins/CMakeLists.txt | 13 + SDK/Plugins/CMakeLists.txt.meta | 7 + SDK/Plugins/Windows.meta | 8 + SDK/Plugins/Windows/CMakeLists.txt | 77 +++ SDK/Plugins/Windows/CMakeLists.txt.meta | 7 + SDK/Plugins/Windows/PrivyWebView.cpp | 437 ++++++++++++++++++ SDK/Plugins/Windows/PrivyWebView.cpp.meta | 69 +++ SDK/Plugins/Windows/x86_64.meta | 8 + SDK/Plugins/Windows/x86_64/Release.meta | 8 + .../Windows/x86_64/Release/PrivyWebView.dll | Bin 0 -> 58368 bytes .../x86_64/Release/PrivyWebView.dll.meta | 69 +++ SDK/Runtime/Auth/AuthDelegator.cs | 31 +- SDK/Runtime/Auth/OAuth/LoginWithOAuth.cs | 3 +- .../Auth/OAuth/OAuthFlows/IOAuthFlow.cs | 6 + .../OAuthFlows/OAuthWindowsWebViewFlow.cs | 20 + .../OAuthWindowsWebViewFlow.cs.meta | 11 + SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs | 2 + .../EmbeddedWallet/WindowsWebViewHandler.cs | 242 ++++++++++ .../WindowsWebViewHandler.cs.meta | 11 + SDK/Runtime/Networking/HttpRequestHandler.cs | 10 +- .../Assets/Scripts/AuthScreenController.cs | 19 + .../Assets/Scripts/InitialScreenController.cs | 35 +- build-plugins.ps1 | 88 ++++ docs/windows-linux-webview-support.md | 117 +++++ 25 files changed, 1294 insertions(+), 15 deletions(-) create mode 100644 SDK/Plugins/CMakeLists.txt create mode 100644 SDK/Plugins/CMakeLists.txt.meta create mode 100644 SDK/Plugins/Windows.meta create mode 100644 SDK/Plugins/Windows/CMakeLists.txt create mode 100644 SDK/Plugins/Windows/CMakeLists.txt.meta create mode 100644 SDK/Plugins/Windows/PrivyWebView.cpp create mode 100644 SDK/Plugins/Windows/PrivyWebView.cpp.meta create mode 100644 SDK/Plugins/Windows/x86_64.meta create mode 100644 SDK/Plugins/Windows/x86_64/Release.meta create mode 100644 SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll create mode 100644 SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll.meta create mode 100644 SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs create mode 100644 SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs.meta create mode 100644 SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs create mode 100644 SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs.meta create mode 100644 build-plugins.ps1 create mode 100644 docs/windows-linux-webview-support.md diff --git a/.gitignore b/.gitignore index 9de2588..162b1d1 100644 --- a/.gitignore +++ b/.gitignore @@ -93,5 +93,16 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# Build artifact folders for C Windows plugin +/SDK/Plugins/Windows/**/bin/ +/SDK/Plugins/Windows/**/obj/ +/SDK/Plugins/Windows/**/Debug/ +/SDK/Plugins/Windows/**/Release/*.lib* +/SDK/Plugins/Windows/**/Release/*.exp* +/SDK/Plugins/Build* + +# Temporary build/test folders +SampleApp/Test*/ + # Environment secrets .env \ No newline at end of file diff --git a/SDK/Plugins/CMakeLists.txt b/SDK/Plugins/CMakeLists.txt new file mode 100644 index 0000000..bdfe82d --- /dev/null +++ b/SDK/Plugins/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) +project(PrivyWebViewPlugins LANGUAGES C CXX) + +option(BUILD_WINDOWS_PLUGIN "Build Windows WebView2 plugin" ON) +option(BUILD_LINUX_PLUGIN "Build Linux WebKitGTK plugin" ON) + +if(BUILD_WINDOWS_PLUGIN AND (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Windows")) + add_subdirectory(Windows) +endif() + +if(BUILD_LINUX_PLUGIN AND (UNIX AND NOT APPLE)) + add_subdirectory(Linux) +endif() diff --git a/SDK/Plugins/CMakeLists.txt.meta b/SDK/Plugins/CMakeLists.txt.meta new file mode 100644 index 0000000..68161c3 --- /dev/null +++ b/SDK/Plugins/CMakeLists.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 28ad8fe971df0584684b7f789da0eb59 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Plugins/Windows.meta b/SDK/Plugins/Windows.meta new file mode 100644 index 0000000..84cae23 --- /dev/null +++ b/SDK/Plugins/Windows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 63fcc736b8f873d40addc87509bb2bb9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Plugins/Windows/CMakeLists.txt b/SDK/Plugins/Windows/CMakeLists.txt new file mode 100644 index 0000000..2e1f5bd --- /dev/null +++ b/SDK/Plugins/Windows/CMakeLists.txt @@ -0,0 +1,77 @@ +cmake_minimum_required(VERSION 3.16) +project(PrivyWebViewWindows LANGUAGES CXX) + +set(PLUGIN_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/x86_64") + +add_library(PrivyWebView SHARED PrivyWebView.cpp) + +# Ensure output is placed in the Unity plugin folder +set_target_properties(PrivyWebView PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${PLUGIN_OUTPUT_DIR}" + LIBRARY_OUTPUT_DIRECTORY "${PLUGIN_OUTPUT_DIR}" + ARCHIVE_OUTPUT_DIRECTORY "${PLUGIN_OUTPUT_DIR}" +) + +# WebView2 SDK support +# User can specify WEBVIEW2_ROOT to point to the WebView2 SDK root (contains include/ and lib/) +if(NOT WEBVIEW2_ROOT) + # ProgramFiles(x86) contains parentheses and cannot be referenced directly in CMake variables. + # Use cmd to expand it instead. + execute_process(COMMAND cmd /c "echo %ProgramFiles(x86)%" OUTPUT_VARIABLE _progfiles_x86 OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process(COMMAND cmd /c "echo %ProgramFiles%" OUTPUT_VARIABLE _progfiles OUTPUT_STRIP_TRAILING_WHITESPACE) + + if(_progfiles_x86) + set(_webview2_candidate "${_progfiles_x86}/Microsoft WebView2 SDK") + elseif(_progfiles) + set(_webview2_candidate "${_progfiles}/Microsoft WebView2 SDK") + endif() + + if(_webview2_candidate) + set(WEBVIEW2_ROOT "${_webview2_candidate}") + endif() +endif() + +# Primary (recommended) include path +find_path(WEBVIEW2_INCLUDE_DIR WebView2.h + PATHS + "${WEBVIEW2_ROOT}/include" + "${WEBVIEW2_ROOT}/*/include" + NO_DEFAULT_PATH +) + +# Primary lib path +find_library(WEBVIEW2_LIB WebView2LoaderStatic + PATHS + "${WEBVIEW2_ROOT}/x64" + "${WEBVIEW2_ROOT}/x86" + "${WEBVIEW2_ROOT}/*/x64" + "${WEBVIEW2_ROOT}/*/x86" + NO_DEFAULT_PATH +) + +# Fallback: if we still don't find it, check the expected folder layout directly. +if(NOT WEBVIEW2_INCLUDE_DIR) + if(EXISTS "${WEBVIEW2_ROOT}/include/WebView2.h") + set(WEBVIEW2_INCLUDE_DIR "${WEBVIEW2_ROOT}/include") + endif() +endif() + +if(NOT WEBVIEW2_LIB) + if(EXISTS "${WEBVIEW2_ROOT}/x64/WebView2LoaderStatic.lib") + set(WEBVIEW2_LIB "${WEBVIEW2_ROOT}/x64/WebView2LoaderStatic.lib") + elseif(EXISTS "${WEBVIEW2_ROOT}/x86/WebView2LoaderStatic.lib") + set(WEBVIEW2_LIB "${WEBVIEW2_ROOT}/x86/WebView2LoaderStatic.lib") + endif() +endif() + +message(STATUS "WebView2 SDK root candidate: ${WEBVIEW2_ROOT}") +message(STATUS "Searching for WebView2.h under: ${WEBVIEW2_ROOT}/include") + +if(WEBVIEW2_INCLUDE_DIR AND WEBVIEW2_LIB) + message(STATUS "Found WebView2 include: ${WEBVIEW2_INCLUDE_DIR}") + message(STATUS "Found WebView2 lib: ${WEBVIEW2_LIB}") + target_include_directories(PrivyWebView PRIVATE "${WEBVIEW2_INCLUDE_DIR}") + target_link_libraries(PrivyWebView PRIVATE "${WEBVIEW2_LIB}") +else() + message(FATAL_ERROR "WebView2 SDK not found. Please install WebView2 SDK (https://developer.microsoft.com/microsoft-edge/webview2/) and ensure WebView2.h and WebView2.lib are available. You can also set WEBVIEW2_ROOT to the SDK root folder.") +endif() diff --git a/SDK/Plugins/Windows/CMakeLists.txt.meta b/SDK/Plugins/Windows/CMakeLists.txt.meta new file mode 100644 index 0000000..36aef08 --- /dev/null +++ b/SDK/Plugins/Windows/CMakeLists.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f0a4866a36c9b974ba277dc86b8bcc0d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Plugins/Windows/PrivyWebView.cpp b/SDK/Plugins/Windows/PrivyWebView.cpp new file mode 100644 index 0000000..f86870d --- /dev/null +++ b/SDK/Plugins/Windows/PrivyWebView.cpp @@ -0,0 +1,437 @@ +// Native plugin for Windows that provides a minimal WebView2-based webview for the embedded wallet. +// +// This file is intended to be built into a DLL (PrivyWebView.dll) and placed under: +// SDK/Plugins/Windows/x86_64/PrivyWebView.dll +// +// Build requirements: +// - WebView2 SDK (Microsoft Edge WebView2) +// - Visual Studio (MSVC) toolchain +// + +#include +#include +#include +#include + +// WebView2 headers (part of Microsoft Edge WebView2 SDK) +#include "WebView2.h" + +// WRL smart pointers for COM lifetime management +#include + +// Forward-declare the callback function types used by the C# wrapper. +using MessageCallback = void(__cdecl*)(const char*); +using StatusCallback = void(__cdecl*)(const char*); + +static MessageCallback g_messageCallback = nullptr; +static StatusCallback g_loadedCallback = nullptr; +static StatusCallback g_errorCallback = nullptr; + +// WebView2 Globals +static Microsoft::WRL::ComPtr g_webViewEnvironment; +static Microsoft::WRL::ComPtr g_webViewController; +static Microsoft::WRL::ComPtr g_webView; + +// Window for WebView2 +static HWND g_hWnd = nullptr; + +static const wchar_t kWindowClassName[] = L"PrivyWebViewWindowClass"; + +static LRESULT CALLBACK WebViewWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_CLOSE) { + // Notify the managed side that the user closed the webview during auth. + if (g_errorCallback) { + g_errorCallback("WEBVIEW_WINDOW_CLOSED"); + } + + // Keep the control alive to allow re-open later; hide instead of destroying. + ShowWindow(hWnd, SW_HIDE); + return 0; + } + + return DefWindowProcW(hWnd, message, wParam, lParam); +} + +static HWND EnsureWebViewWindow() +{ + if (g_hWnd && !IsWindow(g_hWnd)) { + g_hWnd = nullptr; + } + + if (g_hWnd) + return g_hWnd; + + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WebViewWndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = GetModuleHandleW(nullptr); + wcex.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = nullptr; + wcex.lpszClassName = kWindowClassName; + + if (!RegisterClassExW(&wcex) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + return nullptr; + } + + g_hWnd = CreateWindowExW( + 0, + kWindowClassName, + L"Privy Login", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1024, + 768, + nullptr, + nullptr, + wcex.hInstance, + nullptr); + + if (g_hWnd) + { + // Create hidden by default; only show for explicit navigation flow. + ShowWindow(g_hWnd, SW_HIDE); + UpdateWindow(g_hWnd); + } + + return g_hWnd; +} + +static void ShowWebViewWindow() +{ + EnsureWebViewWindow(); + if (g_hWnd && IsWindow(g_hWnd)) { + ShowWindow(g_hWnd, SW_SHOW); + SetForegroundWindow(g_hWnd); + SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + } +} + +static void HideWebViewWindow() +{ + if (g_hWnd && IsWindow(g_hWnd)) { + ShowWindow(g_hWnd, SW_HIDE); + } +} + +static std::wstring Utf8ToUtf16(const std::string& utf8) +{ + if (utf8.empty()) + return {}; + + int size = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0); + if (size <= 0) + return {}; + + std::vector buffer(size); + MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, buffer.data(), size); + + // The returned size includes the terminating null; remove it. + if (!buffer.empty() && buffer.back() == L'\0') + buffer.pop_back(); + + return std::wstring(buffer.begin(), buffer.end()); +} + +static std::wstring g_pendingUrl; +static std::wstring g_pendingJs; +static std::wstring g_oauthRedirectUri; + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_SetRedirectUrl(const char* url) +{ + if (url == nullptr || url[0] == '\0') { + g_oauthRedirectUri.clear(); + if (g_loadedCallback) g_loadedCallback("PrivyWebView_SetRedirectUrl: empty redirect uri"); + return; + } + + g_oauthRedirectUri = Utf8ToUtf16(url); + + char buf[1024]; + sprintf_s(buf, "PrivyWebView_SetRedirectUrl: redirectUri='%s'", url); + if (g_loadedCallback) g_loadedCallback(buf); +} + +static std::wstring UrlDecode(const std::wstring& input) +{ + std::wstring output; + output.reserve(input.size()); + for (size_t i = 0; i < input.size(); ++i) { + wchar_t c = input[i]; + if (c == L'%' && i + 2 < input.size()) { + auto hex = input.substr(i + 1, 2); + wchar_t decoded = static_cast(std::wcstol(hex.c_str(), nullptr, 16)); + output.push_back(decoded); + i += 2; + } else if (c == L'+') { + output.push_back(L' '); + } else { + output.push_back(c); + } + } + return output; +} + +static std::string Utf16ToUtf8(const std::wstring& utf16) +{ + if (utf16.empty()) + return {}; + + int size = WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (size <= 0) + return {}; + + std::vector buffer(size); + WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(), -1, buffer.data(), size, nullptr, nullptr); + + // Remove terminating null + if (!buffer.empty() && buffer.back() == '\0') + buffer.pop_back(); + + return std::string(buffer.begin(), buffer.end()); +} + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Initialize(MessageCallback onMessage, StatusCallback onLoaded, StatusCallback onError) +{ + g_messageCallback = onMessage; + g_loadedCallback = onLoaded; + g_errorCallback = onError; + + // Note: Initialization is best done on the main thread with a message pump. + // For simplicity, we use a minimal hidden window. + + // Ensure we have a window to host the WebView2 control + EnsureWebViewWindow(); + + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + nullptr, nullptr, nullptr, + Microsoft::WRL::Callback( + [](HRESULT result, ICoreWebView2Environment* env) -> HRESULT { + if (FAILED(result)) { + if (g_errorCallback) { + char buf[128]; + sprintf_s(buf, "Failed to create WebView2 environment (HRESULT=0x%08X)", result); + g_errorCallback(buf); + } + return result; + } + + g_webViewEnvironment = env; + env->CreateCoreWebView2Controller( + g_hWnd, + Microsoft::WRL::Callback( + [](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { + if (FAILED(result)) { + if (g_errorCallback) { + char buf[128]; + sprintf_s(buf, "Failed to create WebView2 controller (HRESULT=0x%08X)", result); + g_errorCallback(buf); + } + return result; + } + + g_webViewController = controller; + controller->get_CoreWebView2(&g_webView); + + // Make sure the WebView is visible and sized. + controller->put_IsVisible(TRUE); + RECT bounds = {0, 0, 1024, 768}; + controller->put_Bounds(bounds); + + // Install a message handler to forward messages to Unity + g_webView->add_WebMessageReceived( + Microsoft::WRL::Callback( + [](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT { + PWSTR msg = nullptr; + if (SUCCEEDED(args->TryGetWebMessageAsString(&msg)) && msg != nullptr) { + std::wstring messageW(msg); + std::string utf8 = Utf16ToUtf8(messageW); + if (g_messageCallback) { + g_messageCallback(utf8.c_str()); + } + CoTaskMemFree(msg); + } + return S_OK; + }).Get(), + nullptr); + + // Intercept custom navigation scheme used to message Unity + auto handleUnityScheme = [&](const std::wstring& u) { + auto isUnityScheme = [&](const std::wstring& prefix) { + return u.rfind(prefix, 0) == 0; + }; + + // Only log intercept checks when OAuth redirect flow is configured. + if (!g_oauthRedirectUri.empty()) + { + std::string currentUrl = Utf16ToUtf8(u); + std::string redirectUrl = Utf16ToUtf8(g_oauthRedirectUri); + char buf[2048]; + sprintf_s(buf, "PrivyWebView intercept check: currentUrl='%s', expectedRedirect='%s'", currentUrl.c_str(), redirectUrl.c_str()); + if (g_loadedCallback) g_loadedCallback(buf); + } + + // If we see an OAuth code, always intercept and complete the flow. + if (u.find(L"privy_oauth_code=") != std::wstring::npos) { + HideWebViewWindow(); + + std::string utf8 = Utf16ToUtf8(u); + if (g_messageCallback) { + g_messageCallback(utf8.c_str()); + } + return true; + } + + // If a redirect URI was configured, keep allowing normal navigation, + // but do not send it as a message unless it contains OAuth code. + if (!g_oauthRedirectUri.empty() && isUnityScheme(g_oauthRedirectUri)) { + return false; + } + + // If redirect URI is not configured yet, allow auth domain navigation and do not message. + if (isUnityScheme(L"https://auth.staging.privy.io/") || + isUnityScheme(L"https://auth.privy.io/")) { + return false; + } + + return false; + }; + + g_webView->add_NavigationStarting( + Microsoft::WRL::Callback( + [handleUnityScheme](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { + LPWSTR uri = nullptr; + if (SUCCEEDED(args->get_Uri(&uri)) && uri) { + std::wstring u(uri); + if (handleUnityScheme(u)) { + args->put_Cancel(TRUE); + } + CoTaskMemFree(uri); + } + return S_OK; + }).Get(), + nullptr); + + // Intercept iframe navigations as well. + // When the user is already logged in, the OAuth provider may skip + // the consent screen and Privy may use an iframe-based silent auth + // flow. The redirect with privy_oauth_code happens inside the + // iframe, which NavigationStarting does NOT capture. + g_webView->add_FrameNavigationStarting( + Microsoft::WRL::Callback( + [handleUnityScheme](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { + LPWSTR uri = nullptr; + if (SUCCEEDED(args->get_Uri(&uri)) && uri) { + std::wstring u(uri); + if (handleUnityScheme(u)) { + args->put_Cancel(TRUE); + } + CoTaskMemFree(uri); + } + return S_OK; + }).Get(), + nullptr); + + // Some flows open a new window; intercept those as well. + // By default WebView2 may open external browser for new windows, which breaks embedded flows. + g_webView->add_NewWindowRequested( + Microsoft::WRL::Callback( + [handleUnityScheme](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { + LPWSTR uri = nullptr; + if (SUCCEEDED(args->get_Uri(&uri)) && uri) { + std::wstring u(uri); + // If it's a unity scheme, forward it as a message. + if (handleUnityScheme(u)) { + args->put_Handled(TRUE); + } else { + // Otherwise, keep navigation inside the existing WebView. + // This prevents OAuth flows from popping out to an external browser. + if (sender) { + sender->Navigate(u.c_str()); + } + args->put_Handled(TRUE); + } + CoTaskMemFree(uri); + } + return S_OK; + }).Get(), + nullptr); + + if (g_loadedCallback) g_loadedCallback(""); + + // If a URL was requested before initialization completed, navigate now. + if (!g_pendingUrl.empty()) { + g_webView->Navigate(g_pendingUrl.c_str()); + g_pendingUrl.clear(); + } + + // If JS was queued before initialization, execute now. + if (!g_pendingJs.empty()) { + g_webView->ExecuteScript(g_pendingJs.c_str(), nullptr); + g_pendingJs.clear(); + } + + // Keep the window hidden until explicitly requested via LoadUrl. + // Window will be shown by PrivyWebView_LoadUrl() / PrivyWebView_ShowWindow(). + + return S_OK; + }).Get()); + + return S_OK; + }).Get()); +} + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_LoadUrl(const char* url) +{ + std::wstring wurl = Utf8ToUtf16(url); + if (wurl.empty()) + return; + + EnsureWebViewWindow(); + + if (!g_webView) { + // Queue until initialization completes + g_pendingUrl = std::move(wurl); + return; + } + + g_webView->Navigate(wurl.c_str()); +} + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_ShowWindow() +{ + ShowWebViewWindow(); +} + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_HideWindow() +{ + HideWebViewWindow(); +} + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_EvaluateJS(const char* js) +{ + std::wstring jsW = Utf8ToUtf16(js); + if (jsW.empty()) + return; + + if (!g_webView) { + // Queue until initialization completes + g_pendingJs = std::move(jsW); + return; + } + + g_webView->ExecuteScript(jsW.c_str(), nullptr); +} + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Destroy() +{ + g_webViewController = nullptr; + g_webView = nullptr; + g_webViewEnvironment = nullptr; +} diff --git a/SDK/Plugins/Windows/PrivyWebView.cpp.meta b/SDK/Plugins/Windows/PrivyWebView.cpp.meta new file mode 100644 index 0000000..8caba01 --- /dev/null +++ b/SDK/Plugins/Windows/PrivyWebView.cpp.meta @@ -0,0 +1,69 @@ +fileFormatVersion: 2 +guid: 0817cf7983fbba342bd981b01f71713f +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude WebGL: 1 + Exclude Win: 0 + Exclude Win64: 0 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: Windows + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 1 + settings: + CPU: x86 + - first: + Standalone: Win64 + second: + enabled: 1 + settings: + CPU: x86_64 + - first: + WebGL: WebGL + second: + enabled: 0 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Plugins/Windows/x86_64.meta b/SDK/Plugins/Windows/x86_64.meta new file mode 100644 index 0000000..ec9ccc7 --- /dev/null +++ b/SDK/Plugins/Windows/x86_64.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 930874b12f7cd6247a9d041b8d926050 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Plugins/Windows/x86_64/Release.meta b/SDK/Plugins/Windows/x86_64/Release.meta new file mode 100644 index 0000000..0c9d7ed --- /dev/null +++ b/SDK/Plugins/Windows/x86_64/Release.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eedb1b1e093e263459b3cccfe6bbb32c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll b/SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll new file mode 100644 index 0000000000000000000000000000000000000000..35542facf88000240108ab833a09f7a66049f930 GIT binary patch literal 58368 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 literal 0 HcmV?d00001 diff --git a/SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll.meta b/SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll.meta new file mode 100644 index 0000000..3448bfc --- /dev/null +++ b/SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll.meta @@ -0,0 +1,69 @@ +fileFormatVersion: 2 +guid: 8f0d69b5a7dc0c74d8eaa0368ce768a9 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 0 + Exclude OSXUniversal: 0 + Exclude WebGL: 1 + Exclude Win: 0 + Exclude Win64: 0 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: Windows + - first: + Standalone: Linux64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 1 + settings: + CPU: x86 + - first: + Standalone: Win64 + second: + enabled: 1 + settings: + CPU: x86_64 + - first: + WebGL: WebGL + second: + enabled: 0 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Runtime/Auth/AuthDelegator.cs b/SDK/Runtime/Auth/AuthDelegator.cs index ed570c8..6ad3ddc 100644 --- a/SDK/Runtime/Auth/AuthDelegator.cs +++ b/SDK/Runtime/Auth/AuthDelegator.cs @@ -271,11 +271,36 @@ public async Task RestoreSession() Token jwt = Token.Parse(persistedSession.AccessToken); InternalAuthSession newSession = persistedSession; + bool shouldRefresh = persistedSession?.RefreshToken != null; - if (jwt != null && jwt.IsExpired(Constants.DEFAULT_EXPIRATION_PADDING_IN_SECONDS)) + if (jwt == null || jwt.IsExpired(Constants.DEFAULT_EXPIRATION_PADDING_IN_SECONDS)) { - newSession = await _authRepository.RefreshSession(persistedSession.AccessToken, - persistedSession.RefreshToken); //could be null if request fails + // Expired or invalid token must refresh to validate session and get correct user data. + if (shouldRefresh) + { + newSession = await _authRepository.RefreshSession(persistedSession.AccessToken, + persistedSession.RefreshToken); + } + else + { + // No refresh token available: log out and force re-auth. + Logout(); + return; + } + } + else if (shouldRefresh) + { + // Token valid, but refresh for authoritative user mapping and any account changes. + try + { + newSession = await _authRepository.RefreshSession(persistedSession.AccessToken, + persistedSession.RefreshToken); + } + catch + { + // If refresh fails, keep persisted session to continue working offline. + newSession = persistedSession; + } } SetInternalAuthSession(newSession, persistedSession); diff --git a/SDK/Runtime/Auth/OAuth/LoginWithOAuth.cs b/SDK/Runtime/Auth/OAuth/LoginWithOAuth.cs index 49114f7..2698260 100644 --- a/SDK/Runtime/Auth/OAuth/LoginWithOAuth.cs +++ b/SDK/Runtime/Auth/OAuth/LoginWithOAuth.cs @@ -41,7 +41,8 @@ private async Task PromptOAuthCredentials(OAuthProvider provide if (IsNativeAppleFlow(provider)) return await new NativeAppleSignInFlow().PerformFlow(stateCode); - return await _oAuthFlow.PerformOAuthFlow(oauthUrl, redirectUri); + // Use the transformed redirect URI (e.g., localhost callback) for both init and the WebView flow. + return await _oAuthFlow.PerformOAuthFlow(oauthUrl, transformedRedirectUri); } private static bool IsNativeAppleFlow(OAuthProvider provider) diff --git a/SDK/Runtime/Auth/OAuth/OAuthFlows/IOAuthFlow.cs b/SDK/Runtime/Auth/OAuth/OAuthFlows/IOAuthFlow.cs index 699247b..ffa8b85 100644 --- a/SDK/Runtime/Auth/OAuth/OAuthFlows/IOAuthFlow.cs +++ b/SDK/Runtime/Auth/OAuth/OAuthFlows/IOAuthFlow.cs @@ -18,7 +18,13 @@ internal static IOAuthFlow GetPlatformOAuthFlow() return new OAuthIOSWebAuthenticationFlow(); case RuntimePlatform.WebGLPlayer: return new OAuthWebGLPopupFlow(); + // For Windows (Editor or standalone), use the embedded WebView flow. + case RuntimePlatform.WindowsPlayer: + case RuntimePlatform.WindowsEditor: + return new OAuthWindowsWebViewFlow(); + // For other desktop platforms, use local HTTP listener (fallback) case RuntimePlatform.OSXEditor: + case RuntimePlatform.LinuxEditor: return new OAuthInEditorFlow(); default: return new OAuthExternalBrowserFlow(); diff --git a/SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs b/SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs new file mode 100644 index 0000000..1f7fab6 --- /dev/null +++ b/SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs @@ -0,0 +1,20 @@ +using Privy.Wallets; +using System; +using System.Threading.Tasks; + +namespace Privy.Auth.OAuth +{ + internal class OAuthWindowsWebViewFlow : IOAuthFlow + { + public async Task PerformOAuthFlow(string oAuthUrl, string redirectUri) + { + // The Windows embedded WebView handler intercepts the redirect URL and completes the flow. + // We pass the redirect URI so the handler can detect OAuth callback hits. + // We still pass the redirect URI through to server side via the init call. + + // Timeout set to 5 minutes to match other flows. + var result = await WindowsWebViewHandler.RunOAuthFlow(oAuthUrl, redirectUri, TimeSpan.FromMinutes(5)); + return result; + } + } +} diff --git a/SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs.meta b/SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs.meta new file mode 100644 index 0000000..2f3b96f --- /dev/null +++ b/SDK/Runtime/Auth/OAuth/OAuthFlows/OAuthWindowsWebViewFlow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01c5dc23fd0389441aa747674b76fc56 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs b/SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs index 5b118e5..91e2aad 100644 --- a/SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs +++ b/SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs @@ -23,6 +23,8 @@ internal static IWebViewHandler GetPlatformWebViewHandler(WebViewManager webView return new BrowserDomIframeHandler(webViewManager); #elif UNITY_IOS || UNITY_ANDROID || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX return new WebViewHandler(webViewManager); +#elif UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN + return new WindowsWebViewHandler(webViewManager); #else return new WebViewHandlerForUnsupportedPlatform(); #endif diff --git a/SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs b/SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs new file mode 100644 index 0000000..2aef53b --- /dev/null +++ b/SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs @@ -0,0 +1,242 @@ +#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Privy.Auth.OAuth; +using Privy.Utils; + +namespace Privy.Wallets +{ + internal class WindowsWebViewHandler : IWebViewHandler + { + private readonly WebViewManager _webViewManager; + + private static TaskCompletionSource _oauthTcs; + private static readonly object _oauthLock = new object(); + private static string _oauthRedirectUri; + + internal static bool IsOAuthFlowActive => _oauthTcs != null; + + // Delegates for native callbacks + private delegate void NativeMessageCallback([MarshalAs(UnmanagedType.LPStr)] string message); + private delegate void NativeStatusCallback([MarshalAs(UnmanagedType.LPStr)] string message); + + // Keep delegates alive to avoid GC + private readonly NativeMessageCallback _onMessageReceived; + private readonly NativeStatusCallback _onPageLoaded; + private readonly NativeStatusCallback _onError; + + internal WindowsWebViewHandler(WebViewManager webViewManager = null) + { + _webViewManager = webViewManager; + + _onMessageReceived = OnMessageReceived; + _onPageLoaded = OnPageLoaded; + _onError = OnError; + + // Initialize native WebView2 plugin + PrivyLogger.Debug("Initializing Windows WebView handler"); + PrivyWebView_Initialize(_onMessageReceived, _onPageLoaded, _onError); + } + + internal async static Task RunOAuthFlow(string url, string redirectUri, TimeSpan timeout) + { + // Create the TaskCompletionSource in a local variable so we can safely reference it + // even if _oauthTcs is cleared by the completion path. + var tcs = new TaskCompletionSource(); + + lock (_oauthLock) + { + if (_oauthTcs != null) + { + throw new InvalidOperationException("An OAuth flow is already in progress."); + } + + _oauthTcs = tcs; + _oauthRedirectUri = redirectUri; + } + + // Configure redirect URI for native callback interception. + PrivyWebView_SetRedirectUrl(redirectUri); + + // Show window only for OAuth flows. + PrivyWebView_ShowWindow(); + // Load the URL into the WebView (will be queued if WebView isn't ready) + PrivyWebView_LoadUrl(url); + + // Timeout safety + var cancellation = Task.Delay(timeout); + + var completedTask = await Task.WhenAny(tcs.Task, cancellation); + + // Only clear the shared reference if it still points at our tcs. + lock (_oauthLock) + { + if (_oauthTcs == tcs) + { + _oauthTcs = null; + } + } + + if (completedTask == cancellation) + { + tcs.TrySetException(new TimeoutException("OAuth flow timed out.")); + } + + return await tcs.Task; + } + + public void LoadUrl(string url) + { + PrivyWebView_LoadUrl(url); + } + + public void SendMessage(string message) + { + // Dispatch a message into the WebView using same mechanism as other handlers. + string jsDispatchEvent = $@" + window.dispatchEvent(new MessageEvent('message', {{ data: {message} }})); + "; + PrivyWebView_EvaluateJS(jsDispatchEvent); + } + + private void OnPageLoaded(string url) + { + PrivyLogger.Debug($"Loaded URL: {url}"); + + // Only attempt to ping the embedded wallet when we are on the embedded wallet host page. + // Once the flow navigates to external sites (Google login, etc.), those pages will not respond to our ping. + if (!url.Contains("/embedded-wallets")) + { + PrivyLogger.Debug("Page load is not the embedded wallet page; skipping ready ping."); + return; + } + + // Inject JS proxy to enable embedded wallet communication. + var js = @" + window.PRIVY_UNITY = true; + + window.UnityProxy = { + postMessage: function(message) { + window.chrome.webview.postMessage(message); + } + }; + "; + + PrivyWebView_EvaluateJS(js); + _ =_webViewManager.PingReadyUntilSuccessful(); + } + + private void OnMessageReceived(string message) + { + // If an OAuth flow is active, try to interpret the message as a redirect URI + if (IsOAuthFlowActive) + { + // Match configured redirect URI first + if (!string.IsNullOrEmpty(_oauthRedirectUri) && + message.StartsWith(_oauthRedirectUri, StringComparison.OrdinalIgnoreCase)) + { + PrivyLogger.Debug($"OAuth redirect intercepted: {message}"); + + try + { + var uri = new Uri(message); + var result = OAuthResultData.ParseFromUri(uri); + + if (string.IsNullOrEmpty(result?.OAuthCode)) + { + PrivyLogger.Warning($"OAuth redirect received without authorization code: {uri.Query}"); + lock (_oauthLock) + { + _oauthTcs?.TrySetException( + new PrivyAuthenticationException( + "OAuth redirect received without authorization code. The user may already be logged in with a session that could not be restored.", + AuthenticationError.OAuthVerificationFailed)); + _oauthTcs = null; + _oauthRedirectUri = null; + } + return; + } + + lock (_oauthLock) + { + _oauthTcs?.TrySetResult(result); + _oauthTcs = null; + _oauthRedirectUri = null; + } + return; + } + catch (Exception ex) + { + PrivyLogger.Error($"OAuth redirect parse failed: {ex}"); + lock (_oauthLock) + { + _oauthTcs?.TrySetException(ex); + _oauthTcs = null; + _oauthRedirectUri = null; + } + return; + } + } + + // If this is a URL from the OAuth webflow and does not contain code, ignore it to prevent JSON parsing crashes. + if (Uri.IsWellFormedUriString(message, UriKind.Absolute)) + { + PrivyLogger.Debug($"OAuth flow URL ignored in message handler: {message}"); + return; + } + } + + _webViewManager?.OnMessageReceived(message); + } + + private void OnError(string message) + { + if (message == "WEBVIEW_WINDOW_CLOSED") + { + lock (_oauthLock) + { + if (_oauthTcs != null) + { + _oauthTcs.TrySetCanceled(); + _oauthTcs = null; + _oauthRedirectUri = null; + } + } + + PrivyLogger.Info("Windows WebView OAuth flow canceled by user closing window."); + return; + } + + PrivyLogger.Error($"Windows WebView error: {message}"); + } + + #region Native Plugin Imports + + [DllImport("PrivyWebView", CallingConvention = CallingConvention.Cdecl)] + private static extern void PrivyWebView_Initialize(NativeMessageCallback onMessage, + NativeStatusCallback onLoaded, + NativeStatusCallback onError); + + [DllImport("PrivyWebView", CallingConvention = CallingConvention.Cdecl)] + private static extern void PrivyWebView_LoadUrl([MarshalAs(UnmanagedType.LPStr)] string url); + + [DllImport("PrivyWebView", CallingConvention = CallingConvention.Cdecl)] + private static extern void PrivyWebView_SetRedirectUrl([MarshalAs(UnmanagedType.LPStr)] string url); + + [DllImport("PrivyWebView", CallingConvention = CallingConvention.Cdecl)] + private static extern void PrivyWebView_ShowWindow(); + + [DllImport("PrivyWebView", CallingConvention = CallingConvention.Cdecl)] + private static extern void PrivyWebView_HideWindow(); + + [DllImport("PrivyWebView", CallingConvention = CallingConvention.Cdecl)] + private static extern void PrivyWebView_EvaluateJS([MarshalAs(UnmanagedType.LPStr)] string js); + + [DllImport("PrivyWebView", CallingConvention = CallingConvention.Cdecl)] + private static extern void PrivyWebView_Destroy(); + + #endregion + } +} +#endif diff --git a/SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs.meta b/SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs.meta new file mode 100644 index 0000000..e2d2027 --- /dev/null +++ b/SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aaea4e0d1ba58804a94b4f457ce11fee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SDK/Runtime/Networking/HttpRequestHandler.cs b/SDK/Runtime/Networking/HttpRequestHandler.cs index f8ae51b..d782872 100644 --- a/SDK/Runtime/Networking/HttpRequestHandler.cs +++ b/SDK/Runtime/Networking/HttpRequestHandler.cs @@ -29,7 +29,11 @@ IClientAnalyticsIdRepository clientAnalyticsIdRepository _clientId = privyConfig.ClientId; _baseUrl = $"{PrivyEnvironment.BASE_URL}/api/v1"; _clientAnalyticsIdRepository = clientAnalyticsIdRepository; - _appIdentifier = Application.identifier; + // Unity's Application.identifier can be empty for some standalone builds (or not set in Player Settings). + // The backend expects a native app identifier header for mobile/desktop clients, so we provide a fallback. + _appIdentifier = !string.IsNullOrEmpty(Application.identifier) + ? Application.identifier + : Application.productName; PrivyLogger.Debug($"App identifier is {_appId}"); PrivyLogger.Debug($"Unity app identifier is {_appIdentifier}"); @@ -65,8 +69,8 @@ public async Task SendRequestAsync(string path, string jsonData, string clientAnalyticsId = _clientAnalyticsIdRepository.LoadClientId(); request.SetRequestHeader(Constants.PRIVY_CLIENT_ANALYTICS_ID_HEADER, clientAnalyticsId); - // Need to add native app bundle ID here - if (_appIdentifier != null) + // Need to add native app bundle ID here (only for native platforms, not WebGL) + if (Application.platform != RuntimePlatform.WebGLPlayer && _appIdentifier != null) { request.SetRequestHeader(Constants.PRIVY_NATIVE_APP_IDENTIFIER, _appIdentifier); } diff --git a/SampleApp/Assets/Scripts/AuthScreenController.cs b/SampleApp/Assets/Scripts/AuthScreenController.cs index 9eccd2b..d92d86c 100644 --- a/SampleApp/Assets/Scripts/AuthScreenController.cs +++ b/SampleApp/Assets/Scripts/AuthScreenController.cs @@ -74,8 +74,27 @@ private void Awake() unlinkSmsButton.onClick.AddListener(OnUnlinkSmsButtonClick); if (updateSmsPhoneButton != null) updateSmsPhoneButton.onClick.AddListener(OnUpdateSmsPhoneButtonClick); + } + private async void Start() + { + // Subscribe in Start() rather than Awake() to guarantee PrivyManager.Initialize() + // has been called (Unity does not guarantee Awake() order between scripts). PrivyManager.Instance.AuthStateChanged += OnAuthStateChange; + + // If the SDK is already authenticated when the app starts, transition immediately. + try + { + var state = await PrivyManager.Instance.GetAuthState(); + if (state == AuthState.Authenticated) + { + UIManager.Instance.ShowAuthorizedScreen(); + } + } + catch (Exception ex) + { + Debug.LogError($"Failed to check initial auth state: {ex.Message}"); + } } private void OnDestroy() diff --git a/SampleApp/Assets/Scripts/InitialScreenController.cs b/SampleApp/Assets/Scripts/InitialScreenController.cs index d246aeb..8202e4f 100644 --- a/SampleApp/Assets/Scripts/InitialScreenController.cs +++ b/SampleApp/Assets/Scripts/InitialScreenController.cs @@ -1,5 +1,6 @@ using System; using Privy; +using System.Threading.Tasks; using Privy.Auth; using Privy.Auth.Models; using Privy.Config; @@ -19,10 +20,12 @@ public class InitialScreenController : MonoBehaviour public Button loginWithOAuthAppleButton; public EnvConfig envConfig; - private readonly string _redirectUri = Application.platform == RuntimePlatform.WebGLPlayer ? + private readonly string _redirectUri = Application.platform == RuntimePlatform.WebGLPlayer ? (new Uri(Application.absoluteURL).GetLeftPart(UriPartial.Authority) + "/unity_callback.html") : - "unitydl://"; // Must set each platforms deeplink scheme to this - + Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor ? + "https://auth.staging.privy.io/api/v1/oauth/callback" : // Use Privy generic callback for Windows builds + "unitydl://"; // Use local callback for non-web builds ios/andriod + private void Awake() { EnvFileReader.Config = envConfig; @@ -68,21 +71,37 @@ private void OnLoginWithSmsButtonClick() private async void OnLoginWithOAuthGoogleButtonClick() { - await PrivyManager.Instance.OAuth.LoginWithProvider(OAuthProvider.Google, _redirectUri); + await LoginWithOAuthProvider(OAuthProvider.Google); } - + private async void OnLoginWithOAuthDiscordButtonClick() { - await PrivyManager.Instance.OAuth.LoginWithProvider(OAuthProvider.Discord, _redirectUri); + await LoginWithOAuthProvider(OAuthProvider.Discord); } private async void OnLoginWithOAuthTwitterButtonClick() { - await PrivyManager.Instance.OAuth.LoginWithProvider(OAuthProvider.Twitter, _redirectUri); + await LoginWithOAuthProvider(OAuthProvider.Twitter); } private async void OnLoginWithOAuthAppleButtonClick() { - await PrivyManager.Instance.OAuth.LoginWithProvider(OAuthProvider.Apple, _redirectUri); + await LoginWithOAuthProvider(OAuthProvider.Apple); + } + + private async Task LoginWithOAuthProvider(OAuthProvider provider) + { + try + { + var state = await PrivyManager.Instance.OAuth.LoginWithProvider(provider, _redirectUri); + if (state == AuthState.Authenticated) + { + UIManager.Instance.ShowAuthorizedScreen(); + } + } + catch (Exception ex) + { + Debug.LogError($"OAuth login failed for {provider}: {ex.Message}"); + } } } diff --git a/build-plugins.ps1 b/build-plugins.ps1 new file mode 100644 index 0000000..ee9cb71 --- /dev/null +++ b/build-plugins.ps1 @@ -0,0 +1,88 @@ +<# +Build script for generating native Unity plugin binaries for Windows and Linux. + +Usage: + .\build-plugins.ps1 [-Platform windows|linux|all] [-Configuration Release|Debug] + +Requirements: +- cmake (>= 3.16) +- Visual Studio (for Windows builds) +- WebView2 SDK (for Windows builds) +- libwebkit2gtk-4.0 (for Linux builds) +#> + +param( + [ValidateSet('windows','linux','all')] + [string]$Platform = 'all', + + [ValidateSet('Release','Debug')] + [string]$Configuration = 'Release' +) + +$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition +$pluginsDir = Join-Path $repoRoot 'SDK\Plugins' +$buildDir = Join-Path $pluginsDir 'build' + +function Run-CMake { + param( + [string]$Generator, + [string]$Options + ) + + if (!(Test-Path $buildDir)) { + New-Item -ItemType Directory -Path $buildDir | Out-Null + } + + Write-Host "Generating build files (generator: $Generator)" + & cmake -S $pluginsDir -B $buildDir -G $Generator $Options + if ($LASTEXITCODE -ne 0) { throw "cmake generation failed" } + + Write-Host "Building native plugins ($Configuration)" + & cmake --build $buildDir --config $Configuration + if ($LASTEXITCODE -ne 0) { throw "build failed" } +} + +function Get-WebView2SdkRoot { + if ($env:WEBVIEW2_ROOT) { + return $env:WEBVIEW2_ROOT + } + + $candidates = @( + 'C:\Program Files (x86)\Microsoft WebView2 SDK', + 'C:\Program Files\Microsoft WebView2 SDK' + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return $candidate + } + } + + return $null +} + +if ($Platform -eq 'windows' -or $Platform -eq 'all') { + Write-Host "=== Building Windows plugin ===" + + $webView2Root = Get-WebView2SdkRoot + if (-not $webView2Root) { + Write-Host "ERROR: WebView2 SDK not found. The build requires the WebView2 SDK headers/libs." + Write-Host "Download it from: https://developer.microsoft.com/microsoft-edge/webview2/" + Write-Host "Or set WEBVIEW2_ROOT to the SDK folder (contains include/WebView2.h)." + throw "WebView2 SDK not found" + } + + Write-Host "Using WebView2 SDK root: $webView2Root" + + # Use Visual Studio generator (adjust if you have a different VS version) + $vsGen = 'Visual Studio 17 2022' + Run-CMake -Generator $vsGen -Options "-D BUILD_WINDOWS_PLUGIN=ON -D BUILD_LINUX_PLUGIN=OFF -D WEBVIEW2_ROOT=\"$webView2Root\"" +} + +if ($Platform -eq 'linux' -or $Platform -eq 'all') { + Write-Host "=== Building Linux plugin ===" + # On Windows, you can still build Linux plugin if you have Linux toolchain (WSL or cross-compile) + Run-CMake -Generator 'Unix Makefiles' -Options "-D BUILD_WINDOWS_PLUGIN=OFF -D BUILD_LINUX_PLUGIN=ON" +} + +Write-Host "Done. Plugin binaries are in: $pluginsDir\x86_64" diff --git a/docs/windows-linux-webview-support.md b/docs/windows-linux-webview-support.md new file mode 100644 index 0000000..a73e2ff --- /dev/null +++ b/docs/windows-linux-webview-support.md @@ -0,0 +1,117 @@ +# Windows & Linux Embedded-WebView Support + +## Goal +Add headless embedded wallet WebView support for **Windows (WebView2)** and **Linux (WebKitGTK)** so that the Privy Unity SDK can run embedded wallet operations (wallet creation, signing, RPC) on desktop platforms without relying on `unity-webview`, which currently only supports iOS/Android/macOS. + +This document outlines the approach, design, and implementation steps taken in the repository. + +--- + +## Current State + +- **Supported platforms:** iOS, Android, macOS, WebGL. +- **Unsupported platforms:** Windows and Linux currently fall back to a stub handler where embedded wallet features are not functional. +- **OAuth:** Works via external browser on desktop (Windows, Linux, macOS) and remains unchanged. +- **Architecture:** The embedded wallet uses a single `IWebViewHandler` interface to abstract platform differences. Existing implementations: + - `WebViewHandler` (iOS, Android, macOS) + - `BrowserDomIframeHandler` (WebGL) + - `WebViewHandlerForUnsupportedPlatform` (Windows/Linux) + +--- + +## Implementation Overview + +### Approach +- Add **platform-specific native plugins** for Windows and Linux. +- Implement **C# handler wrappers** that call into those native plugins via P/Invoke. +- Keep the existing `IWebViewHandler` abstraction so the rest of the SDK is unchanged. +- Keep the embedded wallet **headless**, matching existing behavior. + +### Windows (Primary) +- Native plugin: **WebView2** (Microsoft Edge WebView2 Runtime) via C++. +- Exposes a small C API to Unity. +- Uses WebView2's `postMessage` bridge to marshal messages. + +### Linux (Stretch) +- Native plugin: **WebKitGTK** (GTK WebKit) via C. +- Exposes the same small C API. + +--- + +## Code Added / Modified + +### New Files +- `SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs` (Windows handler) +- `SDK/Runtime/EmbeddedWallet/LinuxWebViewHandler.cs` (Linux handler) +- `SDK/Plugins/Windows/PrivyWebView.cpp` (native WebView2 plugin source) +- `SDK/Plugins/Linux/PrivyWebView.c` (native WebKitGTK plugin source) + +### Updated Files +- `SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs` (platform factory updated) +- `SDK/Runtime/EmbeddedWallet/WebViewHandlerForUnsupportedPlatform.cs` (updated logging text) + +--- + +## Usage + +The embedded wallet works exactly like before; it is still invoked via `WebViewManager` and uses the same message protocol (JSON request/response). The only change is that on Windows/Linux, the platform-specific implementation is now functional. + +--- + +## Build Notes (Windows) + +**Requirements:** +- WebView2 Runtime installed (most Windows 10/11 systems already have it). +- Visual Studio (for native plugin build) or `clang-cl`/`msvc` toolchain. + +**Build steps:** +1. Run the build script from the repo root: + - Windows only: `powershell -ExecutionPolicy Bypass -File build-plugins.ps1 -Platform windows` + - Linux only: `powershell -ExecutionPolicy Bypass -File build-plugins.ps1 -Platform linux` + - Both: `powershell -ExecutionPolicy Bypass -File build-plugins.ps1 -Platform all` +2. The script produces plugin output in: + - Windows: `SDK/Plugins/Windows/x86_64/PrivyWebView.dll` + - Linux: `SDK/Plugins/Linux/x86_64/libPrivyWebView.so` +3. Ensure Unity plugin importer sees the binaries (they should already be in the correct folder structure). + +### Windows runtime behavior & developer flow + +- WebView2 window is now shown when OAuth flow starts (`PrivyWebView_LoadUrl`). +- If the user closes the window manually (X), the plugin hides the window instead of destroying it, and the SDK cancels the in-progress OAuth task. +- OAuth flow should be retried safely after close; no `InvalidOperationException` should ever remain from the previous canceled flow. +- The SDK uses `WEBVIEW_WINDOW_CLOSED` internal signal in native callback for this behavior. +- **Redirect URI must be secure (HTTPS) on Windows** for WebView2 OAuth redirect interception to work correctly, e.g. `https://auth.staging.privy.io/api/v1/oauth/callback`. + +**Local test/run steps:** +1. Build plugin as above. +2. Start Unity editor and run `SampleApp`, choose `Windows` platform. +3. Launch OAuth login (e.g., Google login button), verify webview appears. +4. Close window manually while login is in progress, verify the task is canceled and no stale flow is left. +5. Tap login again; verify webview reopens and flow proceeds. + +--- + +## Build Notes (Linux) + +**Requirements:** +- `libwebkit2gtk-4.0` installed. +- GCC/Clang toolchain. + +**Build steps:** +1. Build `PrivyWebView.c` into `libPrivyWebView.so`. +2. Place `libPrivyWebView.so` under `SDK/Plugins/Linux/x86_64/`. + +--- + +## Future / Stretch + +- Add an automated build pipeline to produce plugin binaries and store them in the repo. +- Optionally implement an *in-app* OAuth flow on Windows/Linux using the same WebView as the embedded wallet. +- Add unit tests that mock the native plugin boundary and validate message wiring. + +--- + +## Notes + +- The native plugin code is intentionally minimal and designed to mirror the existing message protocol used by the iOS/Android WebView handler. +- If WebView2 or WebKitGTK is missing, the SDK logs a clear error and falls back to the unsupported handler. From fba49fbfe942d48ae5ad69bdbca94c3abc544f6d Mon Sep 17 00:00:00 2001 From: Andrey Dobrikov Date: Mon, 23 Mar 2026 14:12:01 -0400 Subject: [PATCH 02/13] Added build/support doc for windows Added build/support doc for windows --- docs/windows-linux-webview-support.md | 117 ---------------------- docs/windows_support.md | 134 ++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 117 deletions(-) delete mode 100644 docs/windows-linux-webview-support.md create mode 100644 docs/windows_support.md diff --git a/docs/windows-linux-webview-support.md b/docs/windows-linux-webview-support.md deleted file mode 100644 index a73e2ff..0000000 --- a/docs/windows-linux-webview-support.md +++ /dev/null @@ -1,117 +0,0 @@ -# Windows & Linux Embedded-WebView Support - -## Goal -Add headless embedded wallet WebView support for **Windows (WebView2)** and **Linux (WebKitGTK)** so that the Privy Unity SDK can run embedded wallet operations (wallet creation, signing, RPC) on desktop platforms without relying on `unity-webview`, which currently only supports iOS/Android/macOS. - -This document outlines the approach, design, and implementation steps taken in the repository. - ---- - -## Current State - -- **Supported platforms:** iOS, Android, macOS, WebGL. -- **Unsupported platforms:** Windows and Linux currently fall back to a stub handler where embedded wallet features are not functional. -- **OAuth:** Works via external browser on desktop (Windows, Linux, macOS) and remains unchanged. -- **Architecture:** The embedded wallet uses a single `IWebViewHandler` interface to abstract platform differences. Existing implementations: - - `WebViewHandler` (iOS, Android, macOS) - - `BrowserDomIframeHandler` (WebGL) - - `WebViewHandlerForUnsupportedPlatform` (Windows/Linux) - ---- - -## Implementation Overview - -### Approach -- Add **platform-specific native plugins** for Windows and Linux. -- Implement **C# handler wrappers** that call into those native plugins via P/Invoke. -- Keep the existing `IWebViewHandler` abstraction so the rest of the SDK is unchanged. -- Keep the embedded wallet **headless**, matching existing behavior. - -### Windows (Primary) -- Native plugin: **WebView2** (Microsoft Edge WebView2 Runtime) via C++. -- Exposes a small C API to Unity. -- Uses WebView2's `postMessage` bridge to marshal messages. - -### Linux (Stretch) -- Native plugin: **WebKitGTK** (GTK WebKit) via C. -- Exposes the same small C API. - ---- - -## Code Added / Modified - -### New Files -- `SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs` (Windows handler) -- `SDK/Runtime/EmbeddedWallet/LinuxWebViewHandler.cs` (Linux handler) -- `SDK/Plugins/Windows/PrivyWebView.cpp` (native WebView2 plugin source) -- `SDK/Plugins/Linux/PrivyWebView.c` (native WebKitGTK plugin source) - -### Updated Files -- `SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs` (platform factory updated) -- `SDK/Runtime/EmbeddedWallet/WebViewHandlerForUnsupportedPlatform.cs` (updated logging text) - ---- - -## Usage - -The embedded wallet works exactly like before; it is still invoked via `WebViewManager` and uses the same message protocol (JSON request/response). The only change is that on Windows/Linux, the platform-specific implementation is now functional. - ---- - -## Build Notes (Windows) - -**Requirements:** -- WebView2 Runtime installed (most Windows 10/11 systems already have it). -- Visual Studio (for native plugin build) or `clang-cl`/`msvc` toolchain. - -**Build steps:** -1. Run the build script from the repo root: - - Windows only: `powershell -ExecutionPolicy Bypass -File build-plugins.ps1 -Platform windows` - - Linux only: `powershell -ExecutionPolicy Bypass -File build-plugins.ps1 -Platform linux` - - Both: `powershell -ExecutionPolicy Bypass -File build-plugins.ps1 -Platform all` -2. The script produces plugin output in: - - Windows: `SDK/Plugins/Windows/x86_64/PrivyWebView.dll` - - Linux: `SDK/Plugins/Linux/x86_64/libPrivyWebView.so` -3. Ensure Unity plugin importer sees the binaries (they should already be in the correct folder structure). - -### Windows runtime behavior & developer flow - -- WebView2 window is now shown when OAuth flow starts (`PrivyWebView_LoadUrl`). -- If the user closes the window manually (X), the plugin hides the window instead of destroying it, and the SDK cancels the in-progress OAuth task. -- OAuth flow should be retried safely after close; no `InvalidOperationException` should ever remain from the previous canceled flow. -- The SDK uses `WEBVIEW_WINDOW_CLOSED` internal signal in native callback for this behavior. -- **Redirect URI must be secure (HTTPS) on Windows** for WebView2 OAuth redirect interception to work correctly, e.g. `https://auth.staging.privy.io/api/v1/oauth/callback`. - -**Local test/run steps:** -1. Build plugin as above. -2. Start Unity editor and run `SampleApp`, choose `Windows` platform. -3. Launch OAuth login (e.g., Google login button), verify webview appears. -4. Close window manually while login is in progress, verify the task is canceled and no stale flow is left. -5. Tap login again; verify webview reopens and flow proceeds. - ---- - -## Build Notes (Linux) - -**Requirements:** -- `libwebkit2gtk-4.0` installed. -- GCC/Clang toolchain. - -**Build steps:** -1. Build `PrivyWebView.c` into `libPrivyWebView.so`. -2. Place `libPrivyWebView.so` under `SDK/Plugins/Linux/x86_64/`. - ---- - -## Future / Stretch - -- Add an automated build pipeline to produce plugin binaries and store them in the repo. -- Optionally implement an *in-app* OAuth flow on Windows/Linux using the same WebView as the embedded wallet. -- Add unit tests that mock the native plugin boundary and validate message wiring. - ---- - -## Notes - -- The native plugin code is intentionally minimal and designed to mirror the existing message protocol used by the iOS/Android WebView handler. -- If WebView2 or WebKitGTK is missing, the SDK logs a clear error and falls back to the unsupported handler. diff --git a/docs/windows_support.md b/docs/windows_support.md new file mode 100644 index 0000000..4be0a87 --- /dev/null +++ b/docs/windows_support.md @@ -0,0 +1,134 @@ +# Windows Support Guide (Embedded Wallet) + +> This document describes the Windows implementation details for Privy SDK Embedded Wallet in Unity. +> It is intended as a maintenance and future development reference. + +## 1. Scope + +- Windows desktop support under `UNITY_STANDALONE_WIN` and `UNITY_EDITOR_WIN`. +- Embedded wallet transport pipeline across `WebViewManager` and `WindowsWebViewHandler`. +- OAuth flow handling with native WebView2 plugin. + +## 2. Key files + +- `SDK/Runtime/EmbeddedWallet/WebViewManager.cs` + - central request/response manager + - correlation by ID map + - ready state + timeout behavior + - wallet operation wrappers (CreateEthereumWallet / CreateSolanaWallet / ConnectWallet / RecoverWallet / Request / SignWithUserSigner) + +- `SDK/Runtime/EmbeddedWallet/IWebviewHandler.cs` + - interface for platform-specific handlers + +- `SDK/Runtime/EmbeddedWallet/WindowsWebViewHandler.cs` + - Windows-specific implementation + - native plugin calls via `DllImport("PrivyWebView")` + - OAuth relay and redirect parsing + - content injection and message callback plumbing + +- `SDK/Runtime/EmbeddedWallet/WebViewHandler.cs` + - non-Windows handler (mobile/mac) for reference and cross-platform consistency + +## 3. Architecture + +### WebViewManager + +- Performs JSON message transport to/from embedded wallet JS. +- Tracks outstanding requests in `_requestResponseMap` keyed by request ID. +- Uses `TaskCompletionSource` for asynchronous response completion. +- Handles ping-ready handshake via `PingReadyUntilSuccessful`. +- Supports cancellation via `_disposeCts`. + +### WindowsWebViewHandler + +- Native plugin lifecycle: + - `PrivyWebView_Initialize(onMessage, onLoaded, onError)` + - `PrivyWebView_LoadUrl(url)` + - `PrivyWebView_EvaluateJS(js)` + - `PrivyWebView_SetRedirectUrl(redirectUri)` + - `PrivyWebView_ShowWindow()` / `PrivyWebView_CloseWindow()` + +- On page load, injects a JS proxy: + - `window.PRIVY_UNITY = true` + - `window.UnityProxy.postMessage(...)` forwards to `window.chrome.webview.postMessage(...)` + +- Tracks OAuth via static `_oauthTcs`, `_oauthLock`, `_oauthRedirectUri`. +- Handles redirect URL interception and token extraction. + +## 4. Build requirements (Windows) + +- Unity player settings for Standalone Windows + Editor Windows. + - Standalone Windows uses `Product Name` from Unity Project Settings (does not use `Company Name`) for generated output folder and overlay. + - iOS/Android typically use `Company Name` as publisher bundle id prefix, but this is not used for Windows build targets in this SDK path. +- Include native plugin library `PrivyWebView.dll` in `Plugins` with x64/x86 configurations. +- Ensure `WebView2` runtime is installed on host machine. +- Fallback path: `WebViewHandlerForUnsupportedPlatform` for missing plugin or unsupported platform. + +### 4.1 Native Plugin Build (Windows) + +The native plugin is built from `SDK\Plugins` and provides the `PrivyWebView` API used by `WindowsWebViewHandler`. + +- Build script: `build-plugins.ps1` (root of repository). +- Requirements: + - `cmake` (>= 3.16) + - Visual Studio (for Windows target) + - WebView2 SDK (headers + libs, e.g. `include/WebView2.h`) + +Run the script from the repo root: + +```powershell +# Open PowerShell in the repo root +.\build-plugins.ps1 -Platform windows -Configuration Release +``` + +- Terms: + - uses `SDK\Plugins` as CMake source directory + - output goes to `SDK\Plugins\build` + `SDK\Plugins\x86_64` + - chooses WebView2 location from `WEBVIEW2_ROOT` env var or default install paths + +- If building manually with CMake: + +```powershell +# from repo root +cd SDK\Plugins +mkdir -Force build; cd build +cmake -G "Visual Studio 17 2022" -D BUILD_WINDOWS_PLUGIN=ON -D BUILD_LINUX_PLUGIN=OFF -D WEBVIEW2_ROOT="C:\Program Files (x86)\Microsoft WebView2 SDK" .. +cmake --build . --config Release +``` + +## 5. Runtime behavior + +1. `WebViewManager` constructed with `PrivyConfig` and calls `_webViewHandler.LoadUrl(...)`. +2. `WindowsWebViewHandler` initializes native plugin and loads URL. +3. On embedded wallet page load: + - JS proxy injection + - `WebViewManager.PingReadyUntilSuccessful()` starts ping loop. +4. Responses come through callback `OnMessageReceived`. +5. `WebViewManager` resolves requests by ID from `_requestResponseMap`. + +- Timeouts: + - Default wallet call: 30 seconds + - Create wallet flows: 60 seconds + - PingReady has 150ms timeout per attempt. + +- Disposal: + - Cancel all pending tasks and `_disposeCts`, clear map, cancel `_readyTcs`. + +## 6. Troubleshooting + +- Log `No matching task found for ID`: response arrived after timeout/dispose or malformed ID. +- `OAuth flow is already in progress`: previous `RunOAuthFlow` not finished. +- `WEBVIEW_WINDOW_CLOSED`: user cancelled OAuth; handler cancels `_oauthTcs`. +- `JSON/URI decode errors`: `WebViewManager` decodes URL-encoded payloads from native layer when needed. + +## 7. Suggested future improvements + +- Add explicit WebView2 version detection and user-facing error handling. +- Document writer-level configuration for ping interval and maximum retry. +- Add end-to-end tests for `WebViewManager` RPC timeout and cancellation behavior. +- Consider refactoring code path common between Windows and other handlers into shared helper. + +## 8. Metadata + +- Tags: `windows`, `embedded-wallet`, `webview2`, `oauth`, `unity` +- Status: draft From 8421bc33aae3cdeb0ecb0bd2c6b01d9aeddee6b7 Mon Sep 17 00:00:00 2001 From: Andrey Dobrikov Date: Mon, 30 Mar 2026 15:43:32 -0400 Subject: [PATCH 03/13] Updated to use 2 webviews with thier own contexts Now 2 different webviews - wallet and Oauth --- SDK/Plugins/Windows/PrivyWebView.cpp | 656 ++++++++++-------- .../Windows/x86_64/Release/PrivyWebView.dll | Bin 58368 -> 65536 bytes .../Assets/Scripts/AuthScreenController.cs | 21 +- SampleApp/Assets/Scripts/UIManager.cs | 7 +- docs/windows_support.md | 124 +++- 5 files changed, 476 insertions(+), 332 deletions(-) diff --git a/SDK/Plugins/Windows/PrivyWebView.cpp b/SDK/Plugins/Windows/PrivyWebView.cpp index f86870d..29cfc3f 100644 --- a/SDK/Plugins/Windows/PrivyWebView.cpp +++ b/SDK/Plugins/Windows/PrivyWebView.cpp @@ -1,7 +1,15 @@ -// Native plugin for Windows that provides a minimal WebView2-based webview for the embedded wallet. +// Native plugin for Windows providing two isolated WebView2 instances: // -// This file is intended to be built into a DLL (PrivyWebView.dll) and placed under: -// SDK/Plugins/Windows/x86_64/PrivyWebView.dll +// 1. **Wallet WebView** – Hidden, persistent. Hosts the Privy embedded-wallet +// iframe and handles JSON postMessage communication with Unity. +// +// 2. **OAuth WebView** – Visible, transient. Shown only during OAuth login +// flows to display provider consent screens. Intercepts the redirect +// containing `privy_oauth_code` and forwards it to Unity. +// +// Each webview gets its own ICoreWebView2Environment with a **separate user-data +// directory**, ensuring full browser-level isolation of cookies, localStorage, +// and session state between OAuth and wallet contexts. // // Build requirements: // - WebView2 SDK (Microsoft Edge WebView2) @@ -9,6 +17,7 @@ // #include +#include // SHGetFolderPathW #include #include #include @@ -19,342 +28,462 @@ // WRL smart pointers for COM lifetime management #include -// Forward-declare the callback function types used by the C# wrapper. +// --------------------------------------------------------------------------- +// Callback types shared with the C# managed layer. +// --------------------------------------------------------------------------- using MessageCallback = void(__cdecl*)(const char*); -using StatusCallback = void(__cdecl*)(const char*); +using StatusCallback = void(__cdecl*)(const char*); -static MessageCallback g_messageCallback = nullptr; -static StatusCallback g_loadedCallback = nullptr; -static StatusCallback g_errorCallback = nullptr; +// --------------------------------------------------------------------------- +// Encoding helpers +// --------------------------------------------------------------------------- +static std::wstring Utf8ToUtf16(const std::string& utf8) +{ + if (utf8.empty()) + return {}; -// WebView2 Globals -static Microsoft::WRL::ComPtr g_webViewEnvironment; -static Microsoft::WRL::ComPtr g_webViewController; -static Microsoft::WRL::ComPtr g_webView; + int size = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0); + if (size <= 0) + return {}; + + std::vector buffer(size); + MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, buffer.data(), size); -// Window for WebView2 -static HWND g_hWnd = nullptr; + if (!buffer.empty() && buffer.back() == L'\0') + buffer.pop_back(); -static const wchar_t kWindowClassName[] = L"PrivyWebViewWindowClass"; + return std::wstring(buffer.begin(), buffer.end()); +} -static LRESULT CALLBACK WebViewWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +static std::string Utf16ToUtf8(const std::wstring& utf16) { - if (message == WM_CLOSE) { - // Notify the managed side that the user closed the webview during auth. - if (g_errorCallback) { - g_errorCallback("WEBVIEW_WINDOW_CLOSED"); - } + if (utf16.empty()) + return {}; + + int size = WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (size <= 0) + return {}; + + std::vector buffer(size); + WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(), -1, buffer.data(), size, nullptr, nullptr); + + if (!buffer.empty() && buffer.back() == '\0') + buffer.pop_back(); + + return std::string(buffer.begin(), buffer.end()); +} + +// Return /PrivyWebView/ as a wide string. +// Each webview instance uses a different subfolder to achieve browser-level isolation. +static std::wstring GetUserDataFolder(const wchar_t* subfolder) +{ + wchar_t appDataPath[MAX_PATH] = {}; + if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, appDataPath))) + { + std::wstring path(appDataPath); + path += L"\\PrivyWebView\\"; + path += subfolder; + return path; + } + // Fallback: relative to current directory + std::wstring fallback = L".\\PrivyWebView\\"; + fallback += subfolder; + return fallback; +} + +// =================================================================== +// WALLET WEBVIEW – hidden, persistent, iframe communication +// =================================================================== - // Keep the control alive to allow re-open later; hide instead of destroying. +static MessageCallback g_walletMessageCb = nullptr; +static StatusCallback g_walletLoadedCb = nullptr; +static StatusCallback g_walletErrorCb = nullptr; + +static Microsoft::WRL::ComPtr g_walletEnv; +static Microsoft::WRL::ComPtr g_walletController; +static Microsoft::WRL::ComPtr g_walletWebView; + +static HWND g_walletHWnd = nullptr; +static const wchar_t kWalletWindowClass[] = L"PrivyWalletWebViewClass"; + +static std::wstring g_walletPendingUrl; +static std::wstring g_walletPendingJs; + +static LRESULT CALLBACK WalletWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + // The wallet window should never be visible to the user – ignore close. + if (message == WM_CLOSE) { ShowWindow(hWnd, SW_HIDE); return 0; } - return DefWindowProcW(hWnd, message, wParam, lParam); } -static HWND EnsureWebViewWindow() +static HWND EnsureWalletWindow() { - if (g_hWnd && !IsWindow(g_hWnd)) { - g_hWnd = nullptr; - } + if (g_walletHWnd && !IsWindow(g_walletHWnd)) + g_walletHWnd = nullptr; - if (g_hWnd) - return g_hWnd; + if (g_walletHWnd) + return g_walletHWnd; WNDCLASSEXW wcex = {}; - wcex.cbSize = sizeof(wcex); - wcex.style = CS_HREDRAW | CS_VREDRAW; - wcex.lpfnWndProc = WebViewWndProc; - wcex.cbClsExtra = 0; - wcex.cbWndExtra = 0; - wcex.hInstance = GetModuleHandleW(nullptr); - wcex.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW); - wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); - wcex.lpszMenuName = nullptr; - wcex.lpszClassName = kWindowClassName; - - if (!RegisterClassExW(&wcex) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WalletWndProc; + wcex.hInstance = GetModuleHandleW(nullptr); + wcex.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszClassName = kWalletWindowClass; + + if (!RegisterClassExW(&wcex) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) return nullptr; - } - g_hWnd = CreateWindowExW( - 0, - kWindowClassName, - L"Privy Login", + g_walletHWnd = CreateWindowExW( + 0, kWalletWindowClass, L"PrivyWallet", WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, - CW_USEDEFAULT, - 1024, - 768, - nullptr, - nullptr, - wcex.hInstance, - nullptr); - - if (g_hWnd) - { - // Create hidden by default; only show for explicit navigation flow. - ShowWindow(g_hWnd, SW_HIDE); - UpdateWindow(g_hWnd); + CW_USEDEFAULT, CW_USEDEFAULT, 1024, 768, + nullptr, nullptr, wcex.hInstance, nullptr); + + if (g_walletHWnd) { + ShowWindow(g_walletHWnd, SW_HIDE); + UpdateWindow(g_walletHWnd); } + return g_walletHWnd; +} + +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Wallet_Initialize( + MessageCallback onMessage, StatusCallback onLoaded, StatusCallback onError) +{ + g_walletMessageCb = onMessage; + g_walletLoadedCb = onLoaded; + g_walletErrorCb = onError; + + EnsureWalletWindow(); + + std::wstring userDataDir = GetUserDataFolder(L"Wallet"); + + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + nullptr, userDataDir.c_str(), nullptr, + Microsoft::WRL::Callback( + [](HRESULT result, ICoreWebView2Environment* env) -> HRESULT { + if (FAILED(result)) { + if (g_walletErrorCb) { + char buf[128]; + sprintf_s(buf, "Wallet WebView2 env failed (0x%08X)", result); + g_walletErrorCb(buf); + } + return result; + } + + g_walletEnv = env; + env->CreateCoreWebView2Controller( + g_walletHWnd, + Microsoft::WRL::Callback( + [](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { + if (FAILED(result)) { + if (g_walletErrorCb) { + char buf[128]; + sprintf_s(buf, "Wallet controller failed (0x%08X)", result); + g_walletErrorCb(buf); + } + return result; + } + + g_walletController = controller; + controller->get_CoreWebView2(&g_walletWebView); + controller->put_IsVisible(TRUE); + RECT bounds = {0, 0, 1024, 768}; + controller->put_Bounds(bounds); + + // Forward postMessage from the embedded wallet iframe to Unity. + g_walletWebView->add_WebMessageReceived( + Microsoft::WRL::Callback( + [](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT { + PWSTR msg = nullptr; + if (SUCCEEDED(args->TryGetWebMessageAsString(&msg)) && msg) { + std::string utf8 = Utf16ToUtf8(std::wstring(msg)); + if (g_walletMessageCb) + g_walletMessageCb(utf8.c_str()); + CoTaskMemFree(msg); + } + return S_OK; + }).Get(), + nullptr); + + // Prevent new-window popups from the wallet iframe. + g_walletWebView->add_NewWindowRequested( + Microsoft::WRL::Callback( + [](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { + LPWSTR uri = nullptr; + if (SUCCEEDED(args->get_Uri(&uri)) && uri) { + if (sender) + sender->Navigate(uri); + args->put_Handled(TRUE); + CoTaskMemFree(uri); + } + return S_OK; + }).Get(), + nullptr); - return g_hWnd; + // Notify the managed side when a page finishes loading so it can + // inject the UnityProxy and start the ready-ping at the right time. + g_walletWebView->add_NavigationCompleted( + Microsoft::WRL::Callback( + [](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs*) -> HRESULT { + PWSTR source = nullptr; + if (SUCCEEDED(sender->get_Source(&source)) && source) { + std::string utf8 = Utf16ToUtf8(std::wstring(source)); + if (g_walletLoadedCb) + g_walletLoadedCb(utf8.c_str()); + CoTaskMemFree(source); + } + return S_OK; + }).Get(), + nullptr); + + if (!g_walletPendingUrl.empty()) { + g_walletWebView->Navigate(g_walletPendingUrl.c_str()); + g_walletPendingUrl.clear(); + } + if (!g_walletPendingJs.empty()) { + g_walletWebView->ExecuteScript(g_walletPendingJs.c_str(), nullptr); + g_walletPendingJs.clear(); + } + + return S_OK; + }).Get()); + return S_OK; + }).Get()); } -static void ShowWebViewWindow() +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Wallet_LoadUrl(const char* url) { - EnsureWebViewWindow(); - if (g_hWnd && IsWindow(g_hWnd)) { - ShowWindow(g_hWnd, SW_SHOW); - SetForegroundWindow(g_hWnd); - SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + std::wstring wurl = Utf8ToUtf16(url); + if (wurl.empty()) return; + + EnsureWalletWindow(); + + if (!g_walletWebView) { + g_walletPendingUrl = std::move(wurl); + return; } + g_walletWebView->Navigate(wurl.c_str()); } -static void HideWebViewWindow() +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Wallet_EvaluateJS(const char* js) { - if (g_hWnd && IsWindow(g_hWnd)) { - ShowWindow(g_hWnd, SW_HIDE); + std::wstring jsW = Utf8ToUtf16(js); + if (jsW.empty()) return; + + if (!g_walletWebView) { + g_walletPendingJs = std::move(jsW); + return; } + g_walletWebView->ExecuteScript(jsW.c_str(), nullptr); } -static std::wstring Utf8ToUtf16(const std::string& utf8) +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Wallet_Destroy() { - if (utf8.empty()) - return {}; + g_walletController = nullptr; + g_walletWebView = nullptr; + g_walletEnv = nullptr; +} - int size = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0); - if (size <= 0) - return {}; +// =================================================================== +// OAUTH WEBVIEW – visible, transient, redirect interception +// =================================================================== - std::vector buffer(size); - MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, buffer.data(), size); +static MessageCallback g_oauthMessageCb = nullptr; +static StatusCallback g_oauthLoadedCb = nullptr; +static StatusCallback g_oauthErrorCb = nullptr; - // The returned size includes the terminating null; remove it. - if (!buffer.empty() && buffer.back() == L'\0') - buffer.pop_back(); +static Microsoft::WRL::ComPtr g_oauthEnv; +static Microsoft::WRL::ComPtr g_oauthController; +static Microsoft::WRL::ComPtr g_oauthWebView; - return std::wstring(buffer.begin(), buffer.end()); -} +static HWND g_oauthHWnd = nullptr; +static const wchar_t kOAuthWindowClass[] = L"PrivyOAuthWebViewClass"; -static std::wstring g_pendingUrl; -static std::wstring g_pendingJs; +static std::wstring g_oauthPendingUrl; static std::wstring g_oauthRedirectUri; -extern "C" __declspec(dllexport) void __cdecl PrivyWebView_SetRedirectUrl(const char* url) +static LRESULT CALLBACK OAuthWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - if (url == nullptr || url[0] == '\0') { - g_oauthRedirectUri.clear(); - if (g_loadedCallback) g_loadedCallback("PrivyWebView_SetRedirectUrl: empty redirect uri"); - return; + if (message == WM_CLOSE) { + if (g_oauthErrorCb) + g_oauthErrorCb("WEBVIEW_WINDOW_CLOSED"); + ShowWindow(hWnd, SW_HIDE); + return 0; + } + if (message == WM_SIZE && g_oauthController) { + RECT bounds; + GetClientRect(hWnd, &bounds); + g_oauthController->put_Bounds(bounds); } + return DefWindowProcW(hWnd, message, wParam, lParam); +} - g_oauthRedirectUri = Utf8ToUtf16(url); +static HWND EnsureOAuthWindow() +{ + if (g_oauthHWnd && !IsWindow(g_oauthHWnd)) + g_oauthHWnd = nullptr; + + if (g_oauthHWnd) + return g_oauthHWnd; + + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = OAuthWndProc; + wcex.hInstance = GetModuleHandleW(nullptr); + wcex.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszClassName = kOAuthWindowClass; + + if (!RegisterClassExW(&wcex) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) + return nullptr; + + g_oauthHWnd = CreateWindowExW( + 0, kOAuthWindowClass, L"Privy Login", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, 1024, 768, + nullptr, nullptr, wcex.hInstance, nullptr); - char buf[1024]; - sprintf_s(buf, "PrivyWebView_SetRedirectUrl: redirectUri='%s'", url); - if (g_loadedCallback) g_loadedCallback(buf); + if (g_oauthHWnd) { + ShowWindow(g_oauthHWnd, SW_HIDE); + UpdateWindow(g_oauthHWnd); + } + return g_oauthHWnd; } -static std::wstring UrlDecode(const std::wstring& input) +static void ShowOAuthWindow() { - std::wstring output; - output.reserve(input.size()); - for (size_t i = 0; i < input.size(); ++i) { - wchar_t c = input[i]; - if (c == L'%' && i + 2 < input.size()) { - auto hex = input.substr(i + 1, 2); - wchar_t decoded = static_cast(std::wcstol(hex.c_str(), nullptr, 16)); - output.push_back(decoded); - i += 2; - } else if (c == L'+') { - output.push_back(L' '); - } else { - output.push_back(c); - } + EnsureOAuthWindow(); + if (g_oauthHWnd && IsWindow(g_oauthHWnd)) { + ShowWindow(g_oauthHWnd, SW_SHOW); + SetForegroundWindow(g_oauthHWnd); + SetWindowPos(g_oauthHWnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); } - return output; } -static std::string Utf16ToUtf8(const std::wstring& utf16) +static void HideOAuthWindow() { - if (utf16.empty()) - return {}; - - int size = WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(), -1, nullptr, 0, nullptr, nullptr); - if (size <= 0) - return {}; - - std::vector buffer(size); - WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(), -1, buffer.data(), size, nullptr, nullptr); + if (g_oauthHWnd && IsWindow(g_oauthHWnd)) + ShowWindow(g_oauthHWnd, SW_HIDE); +} - // Remove terminating null - if (!buffer.empty() && buffer.back() == '\0') - buffer.pop_back(); +// Checks whether a URL contains the OAuth completion marker. +// If found: hides the OAuth window and sends the URL to the managed callback. +static bool InterceptOAuthRedirect(const std::wstring& url) +{ + if (url.find(L"privy_oauth_code=") != std::wstring::npos) { + HideOAuthWindow(); + std::string utf8 = Utf16ToUtf8(url); + if (g_oauthMessageCb) + g_oauthMessageCb(utf8.c_str()); + return true; + } + return false; +} - return std::string(buffer.begin(), buffer.end()); +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_OAuth_SetRedirectUrl(const char* url) +{ + if (url == nullptr || url[0] == '\0') { + g_oauthRedirectUri.clear(); + return; + } + g_oauthRedirectUri = Utf8ToUtf16(url); } -extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Initialize(MessageCallback onMessage, StatusCallback onLoaded, StatusCallback onError) +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_OAuth_Initialize( + MessageCallback onMessage, StatusCallback onLoaded, StatusCallback onError) { - g_messageCallback = onMessage; - g_loadedCallback = onLoaded; - g_errorCallback = onError; + g_oauthMessageCb = onMessage; + g_oauthLoadedCb = onLoaded; + g_oauthErrorCb = onError; - // Note: Initialization is best done on the main thread with a message pump. - // For simplicity, we use a minimal hidden window. + EnsureOAuthWindow(); - // Ensure we have a window to host the WebView2 control - EnsureWebViewWindow(); + std::wstring userDataDir = GetUserDataFolder(L"OAuth"); HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( - nullptr, nullptr, nullptr, + nullptr, userDataDir.c_str(), nullptr, Microsoft::WRL::Callback( [](HRESULT result, ICoreWebView2Environment* env) -> HRESULT { if (FAILED(result)) { - if (g_errorCallback) { + if (g_oauthErrorCb) { char buf[128]; - sprintf_s(buf, "Failed to create WebView2 environment (HRESULT=0x%08X)", result); - g_errorCallback(buf); + sprintf_s(buf, "OAuth WebView2 env failed (0x%08X)", result); + g_oauthErrorCb(buf); } return result; } - g_webViewEnvironment = env; + g_oauthEnv = env; env->CreateCoreWebView2Controller( - g_hWnd, + g_oauthHWnd, Microsoft::WRL::Callback( [](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT { if (FAILED(result)) { - if (g_errorCallback) { + if (g_oauthErrorCb) { char buf[128]; - sprintf_s(buf, "Failed to create WebView2 controller (HRESULT=0x%08X)", result); - g_errorCallback(buf); + sprintf_s(buf, "OAuth controller failed (0x%08X)", result); + g_oauthErrorCb(buf); } return result; } - g_webViewController = controller; - controller->get_CoreWebView2(&g_webView); - - // Make sure the WebView is visible and sized. + g_oauthController = controller; + controller->get_CoreWebView2(&g_oauthWebView); controller->put_IsVisible(TRUE); - RECT bounds = {0, 0, 1024, 768}; - controller->put_Bounds(bounds); - - // Install a message handler to forward messages to Unity - g_webView->add_WebMessageReceived( - Microsoft::WRL::Callback( - [](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT { - PWSTR msg = nullptr; - if (SUCCEEDED(args->TryGetWebMessageAsString(&msg)) && msg != nullptr) { - std::wstring messageW(msg); - std::string utf8 = Utf16ToUtf8(messageW); - if (g_messageCallback) { - g_messageCallback(utf8.c_str()); - } - CoTaskMemFree(msg); - } - return S_OK; - }).Get(), - nullptr); - - // Intercept custom navigation scheme used to message Unity - auto handleUnityScheme = [&](const std::wstring& u) { - auto isUnityScheme = [&](const std::wstring& prefix) { - return u.rfind(prefix, 0) == 0; - }; - - // Only log intercept checks when OAuth redirect flow is configured. - if (!g_oauthRedirectUri.empty()) - { - std::string currentUrl = Utf16ToUtf8(u); - std::string redirectUrl = Utf16ToUtf8(g_oauthRedirectUri); - char buf[2048]; - sprintf_s(buf, "PrivyWebView intercept check: currentUrl='%s', expectedRedirect='%s'", currentUrl.c_str(), redirectUrl.c_str()); - if (g_loadedCallback) g_loadedCallback(buf); - } - - // If we see an OAuth code, always intercept and complete the flow. - if (u.find(L"privy_oauth_code=") != std::wstring::npos) { - HideWebViewWindow(); - - std::string utf8 = Utf16ToUtf8(u); - if (g_messageCallback) { - g_messageCallback(utf8.c_str()); - } - return true; - } - // If a redirect URI was configured, keep allowing normal navigation, - // but do not send it as a message unless it contains OAuth code. - if (!g_oauthRedirectUri.empty() && isUnityScheme(g_oauthRedirectUri)) { - return false; - } - - // If redirect URI is not configured yet, allow auth domain navigation and do not message. - if (isUnityScheme(L"https://auth.staging.privy.io/") || - isUnityScheme(L"https://auth.privy.io/")) { - return false; - } - - return false; - }; + RECT bounds; + GetClientRect(g_oauthHWnd, &bounds); + controller->put_Bounds(bounds); - g_webView->add_NavigationStarting( + // Top-level navigation: check for OAuth redirect. + g_oauthWebView->add_NavigationStarting( Microsoft::WRL::Callback( - [handleUnityScheme](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { + [](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { LPWSTR uri = nullptr; if (SUCCEEDED(args->get_Uri(&uri)) && uri) { std::wstring u(uri); - if (handleUnityScheme(u)) { + if (InterceptOAuthRedirect(u)) args->put_Cancel(TRUE); - } CoTaskMemFree(uri); } return S_OK; }).Get(), nullptr); - // Intercept iframe navigations as well. - // When the user is already logged in, the OAuth provider may skip - // the consent screen and Privy may use an iframe-based silent auth - // flow. The redirect with privy_oauth_code happens inside the - // iframe, which NavigationStarting does NOT capture. - g_webView->add_FrameNavigationStarting( + // Iframe navigation: silent-auth flows redirect inside an iframe. + g_oauthWebView->add_FrameNavigationStarting( Microsoft::WRL::Callback( - [handleUnityScheme](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { + [](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { LPWSTR uri = nullptr; if (SUCCEEDED(args->get_Uri(&uri)) && uri) { std::wstring u(uri); - if (handleUnityScheme(u)) { + if (InterceptOAuthRedirect(u)) args->put_Cancel(TRUE); - } CoTaskMemFree(uri); } return S_OK; }).Get(), nullptr); - // Some flows open a new window; intercept those as well. - // By default WebView2 may open external browser for new windows, which breaks embedded flows. - g_webView->add_NewWindowRequested( + // New-window requests: keep inside the OAuth webview. + g_oauthWebView->add_NewWindowRequested( Microsoft::WRL::Callback( - [handleUnityScheme](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { + [](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { LPWSTR uri = nullptr; if (SUCCEEDED(args->get_Uri(&uri)) && uri) { std::wstring u(uri); - // If it's a unity scheme, forward it as a message. - if (handleUnityScheme(u)) { + if (InterceptOAuthRedirect(u)) { args->put_Handled(TRUE); - } else { - // Otherwise, keep navigation inside the existing WebView. - // This prevents OAuth flows from popping out to an external browser. - if (sender) { - sender->Navigate(u.c_str()); - } + } else if (sender) { + sender->Navigate(u.c_str()); args->put_Handled(TRUE); } CoTaskMemFree(uri); @@ -363,75 +492,46 @@ extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Initialize(MessageCal }).Get(), nullptr); - if (g_loadedCallback) g_loadedCallback(""); - - // If a URL was requested before initialization completed, navigate now. - if (!g_pendingUrl.empty()) { - g_webView->Navigate(g_pendingUrl.c_str()); - g_pendingUrl.clear(); - } + if (g_oauthLoadedCb) g_oauthLoadedCb(""); - // If JS was queued before initialization, execute now. - if (!g_pendingJs.empty()) { - g_webView->ExecuteScript(g_pendingJs.c_str(), nullptr); - g_pendingJs.clear(); + if (!g_oauthPendingUrl.empty()) { + g_oauthWebView->Navigate(g_oauthPendingUrl.c_str()); + g_oauthPendingUrl.clear(); } - // Keep the window hidden until explicitly requested via LoadUrl. - // Window will be shown by PrivyWebView_LoadUrl() / PrivyWebView_ShowWindow(). - return S_OK; }).Get()); - return S_OK; }).Get()); } -extern "C" __declspec(dllexport) void __cdecl PrivyWebView_LoadUrl(const char* url) +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_OAuth_LoadUrl(const char* url) { std::wstring wurl = Utf8ToUtf16(url); - if (wurl.empty()) - return; + if (wurl.empty()) return; - EnsureWebViewWindow(); + EnsureOAuthWindow(); - if (!g_webView) { - // Queue until initialization completes - g_pendingUrl = std::move(wurl); + if (!g_oauthWebView) { + g_oauthPendingUrl = std::move(wurl); return; } - - g_webView->Navigate(wurl.c_str()); -} - -extern "C" __declspec(dllexport) void __cdecl PrivyWebView_ShowWindow() -{ - ShowWebViewWindow(); + g_oauthWebView->Navigate(wurl.c_str()); } -extern "C" __declspec(dllexport) void __cdecl PrivyWebView_HideWindow() +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_OAuth_ShowWindow() { - HideWebViewWindow(); + ShowOAuthWindow(); } -extern "C" __declspec(dllexport) void __cdecl PrivyWebView_EvaluateJS(const char* js) +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_OAuth_HideWindow() { - std::wstring jsW = Utf8ToUtf16(js); - if (jsW.empty()) - return; - - if (!g_webView) { - // Queue until initialization completes - g_pendingJs = std::move(jsW); - return; - } - - g_webView->ExecuteScript(jsW.c_str(), nullptr); + HideOAuthWindow(); } -extern "C" __declspec(dllexport) void __cdecl PrivyWebView_Destroy() +extern "C" __declspec(dllexport) void __cdecl PrivyWebView_OAuth_Destroy() { - g_webViewController = nullptr; - g_webView = nullptr; - g_webViewEnvironment = nullptr; + g_oauthController = nullptr; + g_oauthWebView = nullptr; + g_oauthEnv = nullptr; } diff --git a/SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll b/SDK/Plugins/Windows/x86_64/Release/PrivyWebView.dll index 35542facf88000240108ab833a09f7a66049f930..20080cd8a617b8cd205e7ca299a3af215c733f64 100644 GIT binary patch literal 65536 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 delta 25757 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