From d140c13afa3d249bd4bab681288cc84b6575dfbf Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 9 Jun 2025 18:16:10 -0700 Subject: [PATCH 1/3] Add jni target to ReactCxxPlatform Summary: changelog: [internal] Differential Revision: D76240376 --- .../ReactCxxPlatform/react/jni/JniHelper.cpp | 28 +++++++++++++++++++ .../ReactCxxPlatform/react/jni/JniHelper.h | 19 +++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h diff --git a/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp new file mode 100644 index 000000000000..a3689d5d0949 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +namespace facebook::react { + +jobject getApplication(JNIEnv* env) { + auto activityThreadClass = env->FindClass("android/app/ActivityThread"); + auto currentApplicationMethodID = env->GetStaticMethodID( + activityThreadClass, "currentApplication", "()Landroid/app/Application;"); + return env->CallStaticObjectMethod( + activityThreadClass, currentApplicationMethodID); +} + +jni::alias_ref getContext() { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto application = getApplication(env); + return facebook::jni::wrap_alias( + static_cast(application)); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h new file mode 100644 index 000000000000..8db809186f31 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/jni/JniHelper.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +jobject getApplication(JNIEnv* env); + +jni::alias_ref getContext(); + +} // namespace facebook::react From dec213b06d2df735d99a76a007cef57cd6208cef Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 9 Jun 2025 18:16:10 -0700 Subject: [PATCH 2/3] Add http target to ReactCxxPlatform Summary: changelog: [internal] Differential Revision: D76240550 --- .../react/http/IHttpClient.cpp | 13 +++ .../ReactCxxPlatform/react/http/IHttpClient.h | 89 +++++++++++++++++++ .../react/http/IWebSocketClient.cpp | 13 +++ .../react/http/IWebSocketClient.h | 47 ++++++++++ 4 files changed, 162 insertions(+) create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h diff --git a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp new file mode 100644 index 000000000000..c19f481ee22d --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp @@ -0,0 +1,13 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char HttpClientFactoryKey[] = "HttpClientFactory"; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h new file mode 100644 index 000000000000..9b3d78b5c9d6 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace http { + +using Headers = std::vector>; + +struct FormDataField { + std::string fieldName; + Headers headers; + std::optional string; + std::optional uri; +}; + +using FormData = std::vector; + +struct Body { + std::optional string; + std::optional blob; + std::optional formData; + std::optional base64; +}; + +using OnUploadProgress = std::function; +using OnResponse = std::function; +using OnBody = std::function body)>; +using OnBodyIncremental = std::function body)>; +using OnBodyProgress = std::function; +using OnResponseComplete = + std::function; + +struct NetworkCallbacks { + OnUploadProgress onUploadProgress{nullptr}; + OnResponse onResponse{nullptr}; + OnBody onBody{nullptr}; + OnBodyIncremental onBodyIncremental{nullptr}; + OnBodyProgress onBodyProgress{nullptr}; + OnResponseComplete onResponseComplete{nullptr}; + bool sendIncrementalUpdates{false}; + bool sendProgressUpdates{false}; +}; + +struct IRequestToken { + virtual ~IRequestToken() = default; + + virtual void cancel() noexcept = 0; +}; + +} // namespace http + +struct IHttpClient { + virtual ~IHttpClient() = default; + + virtual std::unique_ptr sendRequest( + http::NetworkCallbacks&& callback, + const std::string& method, + const std::string& url, + const http::Headers& headers = {}, + const http::Body& body = {}, + uint32_t timeout = 0, + std::optional loggingId = std::nullopt) = 0; +}; + +extern const char HttpClientFactoryKey[]; + +using HttpClientFactory = std::function()>; + +HttpClientFactory getHttpClientFactory(); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp new file mode 100644 index 000000000000..93800a35a7b7 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp @@ -0,0 +1,13 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char WebSocketClientFactoryKey[] = "WebSocketClientFactory"; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h new file mode 100644 index 000000000000..a2ec1b0859bb --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient { + public: + using OnConnectCallback = std::function; + using OnClosedCallback = std::function; + using OnMessageCallback = std::function; + + virtual ~IWebSocketClient() = default; + + virtual void setOnClosedCallback(OnClosedCallback&& callback) noexcept = 0; + + virtual void setOnMessageCallback(OnMessageCallback&& callback) noexcept = 0; + + virtual void connect( + const std::string& url, + OnConnectCallback&& = nullptr) = 0; + + virtual void close(const std::string& reason) = 0; + + virtual void send(const std::string& message) = 0; + + virtual void ping() = 0; +}; + +extern const char WebSocketClientFactoryKey[]; + +using WebSocketClientFactory = + std::function()>; + +WebSocketClientFactory getWebSocketClientFactory(); + +} // namespace facebook::react From 14a76d30173eeb378ace7bd15b0955c5a0d4e15a Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 9 Jun 2025 18:47:49 -0700 Subject: [PATCH 3/3] Add io target to ReactCxxPlatform (#51900) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51900 changelog: [internal] Reviewed By: andrewdacenko Differential Revision: D76240623 --- .../react/io/ImageLoaderModule.cpp | 52 +++ .../react/io/ImageLoaderModule.h | 48 +++ .../react/io/NetworkingModule.cpp | 354 ++++++++++++++++++ .../react/io/NetworkingModule.h | 147 ++++++++ .../react/io/ResourceLoader.cpp | 76 ++++ .../react/io/ResourceLoader.h | 32 ++ .../react/io/WebSocketModule.cpp | 157 ++++++++ .../react/io/WebSocketModule.h | 64 ++++ .../platform/android/AssetManagerHelpers.cpp | 60 +++ .../io/platform/android/AssetManagerHelpers.h | 21 ++ .../io/platform/android/ResourceLoader.cpp | 71 ++++ .../react/io/platform/cxx/ResourceLoader.cpp | 37 ++ .../react/io/tests/NetworkingModuleTests.cpp | 126 +++++++ 13 files changed, 1245 insertions(+) create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp create mode 100644 packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp diff --git a/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp new file mode 100644 index 000000000000..1265eb259e3d --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ImageLoaderModule.h" + +namespace facebook::react { + +// TODO: T170340321 - actual implementation + +jsi::Object ImageLoaderModule::getConstants(jsi::Runtime& rt) { + return jsi::Object(rt); +} + +void ImageLoaderModule::abortRequest(jsi::Runtime& rt, int32_t requestId) {} + +AsyncPromise ImageLoaderModule::getSize( + jsi::Runtime& rt, + const std::string& /*uri*/) { + auto promise = AsyncPromise(rt, jsInvoker_); + promise.resolve({.width = 0.0, .height = 0.0}); + return promise; +} + +AsyncPromise ImageLoaderModule::getSizeWithHeaders( + jsi::Runtime& rt, + const std::string& /*uri*/, + jsi::Object /*headers*/) { + auto promise = AsyncPromise(rt, jsInvoker_); + promise.resolve({.width = 0.0, .height = 0.0}); + return promise; +} + +AsyncPromise ImageLoaderModule::prefetchImage( + jsi::Runtime& rt, + const std::string& /*uri*/, + int32_t /*requestId*/) { + auto promise = AsyncPromise(rt, jsInvoker_); + promise.resolve(false); + return promise; +} + +jsi::Object ImageLoaderModule::queryCache( + jsi::Runtime& rt, + const std::vector& /*uris*/) { + return jsi::Object(rt); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h new file mode 100644 index 000000000000..932ce9d18700 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ImageLoaderModule.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +using ImageSize = NativeImageLoaderAndroidImageSize; + +template <> +struct Bridging + : NativeImageLoaderAndroidImageSizeBridging {}; + +class ImageLoaderModule + : public NativeImageLoaderAndroidCxxSpec { + public: + explicit ImageLoaderModule(std::shared_ptr jsInvoker) + : NativeImageLoaderAndroidCxxSpec(jsInvoker) {} + + jsi::Object getConstants(jsi::Runtime& rt); + void abortRequest(jsi::Runtime& rt, int32_t requestId); + + AsyncPromise getSize(jsi::Runtime& rt, const std::string& uri); + + AsyncPromise getSizeWithHeaders( + jsi::Runtime& rt, + const std::string& uri, + jsi::Object headers); + + AsyncPromise + prefetchImage(jsi::Runtime& rt, const std::string& uri, int32_t requestId); + + jsi::Object queryCache( + jsi::Runtime& rt, + const std::vector& uris); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp new file mode 100644 index 000000000000..7f685f51b1bd --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.cpp @@ -0,0 +1,354 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "NetworkingModule.h" + +#include + +namespace facebook::react { + +// The requests abstraction is here to deal with a family of race conditions +// which can happen due to the async nature of this code: +// +// * store token vs callback +// * callback vs cancel +// * stop vs everything +// +// All of the access to the Requests data is protected by a mutex, so it is +// effectively serialized. The code takes care to handle execution in any +// order, so long as reserve for an id precedes any other operation on that id; +// the tests verify that behavior is correct for all use cases. + +Requests::~Requests() { + react_native_assert(isStopped() && "Requests::stop() was not called"); +} + +bool Requests::reserve(uint32_t id) { + if (stopped_) { + return false; + } + std::lock_guard lock(tokensMutex_); + tokens_.emplace(id, nullptr); + return true; +} + +void Requests::store(uint32_t id, std::unique_ptr token) { + // Three possibilities here: + + // 1. HttpClient is shutting down. Cancel the token, don't save it. + // 2. The entry in tokens_ has been erased, which means the callbacks have + // run. We cancel, which should be harmless, just in case. Nothing to save. + // 3. HttpClient and the request are still running. Save the token. + + { + if (stopped_) { + // option 1: fall past the lock and cancel + } else { + std::lock_guard lock(tokensMutex_); + if (auto iter = tokens_.find(id); iter == tokens_.end()) { + // option 2: fall past the lock and cancel + } else { + // option 3: save the token + iter->second = std::move(token); + return; + } + } + } + + if (token) { + token->cancel(); + } +} + +bool Requests::erase(uint32_t id) { + if (stopped_) { + return false; + } + std::lock_guard lock(tokensMutex_); + tokens_.erase(id); + return true; +} + +void Requests::cancel(uint32_t id) { + std::lock_guard lock(tokensMutex_); + if (auto iter = tokens_.find(id); iter != tokens_.end()) { + if (iter->second) { + iter->second->cancel(); + } + tokens_.erase(iter); + } +} + +void Requests::stop() { + stopped_ = true; + decltype(tokens_) tokensCopy; + { + std::lock_guard lock(tokensMutex_); + tokensCopy.swap(tokens_); + } + + for (const auto& token : tokensCopy) { + if (token.second) { + token.second->cancel(); + } + } +} + +bool Requests::isStopped() { + if (stopped_) { + std::lock_guard lock(tokensMutex_); + assert(tokens_.empty()); + } + return stopped_; +} + +NetworkingModule::NetworkingModule( + std::shared_ptr jsInvoker, + const HttpClientFactory& httpClientFactory) + : NativeNetworkingAndroidCxxSpec(jsInvoker) { + httpClient_ = httpClientFactory(); +} + +NetworkingModule::~NetworkingModule() { + requests_.stop(); +} + +void NetworkingModule::sendRequest( + jsi::Runtime& /*rt*/, + const std::string& method, + const std::string& url, + uint32_t requestId, + const http::Headers& headers, + const http::Body& body, + const std::string& responseType, + bool useIncrementalUpdates, + uint32_t timeout, + bool /*withCredentials*/) { + if (!requests_.reserve(requestId)) { + // If the NetworkingModule is shutting down, don't invoke the callback. + return; + } + + bool sendIncrementalUpdates = responseType == "text" && useIncrementalUpdates; + bool sendProgressUpdates = responseType != "text" && useIncrementalUpdates; + + http::NetworkCallbacks callbacks{ + .onUploadProgress = + [weakThis = weak_from_this(), requestId]( + int64_t progress, int64_t total) { + if (auto strongThis = weakThis.lock()) { + strongThis->didSendNetworkData(requestId, progress, total); + } + }, + .onResponse = + [weakThis = weak_from_this(), requestId, url]( + uint32_t responseCode, const http::Headers& headers) { + if (auto strongThis = weakThis.lock()) { + strongThis->didReceiveNetworkResponse( + requestId, responseCode, headers, url); + } + }, + .onBody = + [weakThis = weak_from_this(), requestId, responseType]( + std::unique_ptr data) { + if (auto strongThis = weakThis.lock()) { + try { + strongThis->didReceiveNetworkData( + requestId, responseType, std::move(data)); + } catch (const std::exception& e) { + LOG(ERROR) << "Failed to parse network response body: " + << e.what(); + } catch (...) { + LOG(ERROR) << "Failed to parse network response body."; + } + } + }, + .onBodyIncremental = + [weakThis = weak_from_this(), requestId]( + int64_t progress, + int64_t total, + std::unique_ptr body) { + int64_t bytesRead = 0; + if (auto strongThis = weakThis.lock()) { + bytesRead = strongThis->didReceiveNetworkIncrementalData( + requestId, std::move(body), progress, total); + } + return bytesRead; + }, + .onBodyProgress = + [weakThis = weak_from_this(), requestId]( + int64_t loaded, int64_t total) { + if (auto strongThis = weakThis.lock()) { + strongThis->didReceiveNetworkDataProgress( + requestId, loaded, total); + } + }, + .onResponseComplete = + [weakThis = weak_from_this(), requestId]( + const std::string& error, bool timeout) { + if (auto strongThis = weakThis.lock()) { + strongThis->didCompleteNetworkResponse(requestId, error, timeout); + } + }, + .sendIncrementalUpdates = sendIncrementalUpdates, + .sendProgressUpdates = sendProgressUpdates}; + + auto token = httpClient_->sendRequest( + std::move(callbacks), + method, + url, + headers, + body, + timeout, + std::to_string(requestId)); + if (token) { + requests_.store(requestId, std::move(token)); + } +} + +void NetworkingModule::didSendNetworkData( + uint32_t requestId, + int64_t progress, + int64_t total) { + if (requests_.isStopped()) { + return; + } + emitDeviceEvent( + "didSendNetworkData", + [requestId, progress, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + auto result = std::make_tuple( + requestId, + static_cast(progress), + static_cast(total)); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + }); +} + +void NetworkingModule::didReceiveNetworkResponse( + uint32_t requestId, + uint32_t statusCode, + const http::Headers& headers, + const std::string& responseUrl) noexcept { + if (requests_.isStopped()) { + return; + } + emitDeviceEvent( + "didReceiveNetworkResponse", + [requestId, statusCode, headers, responseUrl, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + auto result = + std::make_tuple(requestId, statusCode, headers, responseUrl); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + }); +} + +int64_t NetworkingModule::didReceiveNetworkIncrementalData( + uint32_t requestId, + std::unique_ptr buf, + int64_t progress, + int64_t total) { + auto data = buf->moveToFbString().toStdString(); + auto bytesRead = data.size(); + emitDeviceEvent( + "didReceiveNetworkIncrementalData", + [requestId, data, progress, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, + static_cast(requestId), + data, + static_cast(progress), + static_cast(total))); + }); + return bytesRead; +}; + +void NetworkingModule::didReceiveNetworkData( + uint32_t requestId, + const std::string& /*responseType*/, + std::unique_ptr buf) { + if (requests_.isStopped()) { + return; + } + + auto responseData = buf->toString(); + emitDeviceEvent( + "didReceiveNetworkData", + [requestId, + responseData = std::move(responseData), + jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, static_cast(requestId), responseData)); + }); +} + +void NetworkingModule::didReceiveNetworkDataProgress( + uint32_t requestId, + int64_t bytesRead, + int64_t total) { + emitDeviceEvent( + "didReceiveNetworkDataProgress", + [requestId, bytesRead, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, + static_cast(requestId), + static_cast(bytesRead), + static_cast(total))); + }); +} + +void NetworkingModule::didCompleteNetworkResponse( + uint32_t requestId, + const std::string& error, + bool timeoutError) { + if (requests_.isStopped() || !requests_.erase(requestId)) { + // If the HttpClient is shutting down, don't invoke the callback. + return; + } + emitDeviceEvent( + "didCompleteNetworkResponse", + [requestId, error, timeoutError, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + if (error.empty()) { + auto result = std::make_tuple(static_cast(requestId)); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + } else { + auto result = std::make_tuple( + static_cast(requestId), error, timeoutError); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + } + }); +} + +void NetworkingModule::abortRequest(jsi::Runtime& /*rt*/, uint32_t requestId) { + requests_.cancel(requestId); +} + +void NetworkingModule::clearCookies( + jsi::Runtime& /*rt*/, + const AsyncCallback& /*callback*/) { + LOG(INFO) << "NetworkingModule::clearCookies"; +} + +void NetworkingModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& /*eventName*/) {} + +void NetworkingModule::removeListeners( + jsi::Runtime& /*rt*/, + uint32_t /*count*/) {} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h new file mode 100644 index 000000000000..e2e09005a7b0 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/NetworkingModule.h @@ -0,0 +1,147 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static http::FormDataField fromJs( + jsi::Runtime& rt, + const jsi::Object& value, + const std::shared_ptr& jsInvoker) { + auto fieldName = bridging::fromJs( + rt, value.getProperty(rt, "fieldName"), jsInvoker); + auto headers = bridging::fromJs( + rt, value.getProperty(rt, "headers"), jsInvoker); + auto string = bridging::fromJs>( + rt, value.getProperty(rt, "string"), jsInvoker); + auto uri = bridging::fromJs>( + rt, value.getProperty(rt, "uri"), jsInvoker); + return http::FormDataField{ + .fieldName = fieldName, + .headers = headers, + .string = string, + .uri = uri}; + } +}; + +template <> +struct Bridging { + static http::Body fromJs( + jsi::Runtime& rt, + const jsi::Object& value, + const std::shared_ptr& jsInvoker) { + return http::Body{ + .string = bridging::fromJs>( + rt, value.getProperty(rt, "string"), jsInvoker), + .blob = bridging::fromJs>( + rt, value.getProperty(rt, "blob"), jsInvoker), + .formData = bridging::fromJs>( + rt, value.getProperty(rt, "formData"), jsInvoker), + .base64 = bridging::fromJs>( + rt, value.getProperty(rt, "base64"), jsInvoker), + + }; + } +}; + +class Requests { + public: + Requests() noexcept = default; + ~Requests(); + Requests(Requests& other) = delete; + Requests& operator=(Requests& other) = delete; + Requests(Requests&& other) = delete; + Requests& operator=(Requests&& other) = delete; + + bool reserve(uint32_t id); + void store(uint32_t id, std::unique_ptr token); + bool erase(uint32_t id); + void cancel(uint32_t id); + void stop(); + bool isStopped(); + + private: + std::mutex tokensMutex_; + std::unordered_map> tokens_; + std::atomic stopped_{false}; +}; + +// Implementation of "Networking" TurboModule +class NetworkingModule + : public NativeNetworkingAndroidCxxSpec, + public std::enable_shared_from_this { + public: + NetworkingModule( + std::shared_ptr jsInvoker, + const HttpClientFactory& httpClientFactory); + + ~NetworkingModule() override; + + void sendRequest( + jsi::Runtime& rt, + const std::string& method, + const std::string& url, + uint32_t requestId, + const http::Headers& headers, + const http::Body& body, + const std::string& responseType, + bool useIncrementalUpdates, + uint32_t timeout, + bool withCredentials); + + void abortRequest(jsi::Runtime& rt, uint32_t requestId); + + void clearCookies(jsi::Runtime& rt, const AsyncCallback& callback); + + // RCTEventEmitter + void addListener(jsi::Runtime& rt, const std::string& eventName); + void removeListeners(jsi::Runtime& rt, uint32_t count); + + private: + void didSendNetworkData(uint32_t requestId, int64_t progress, int64_t total); + + void didReceiveNetworkResponse( + uint32_t requestId, + uint32_t statusCode, + const http::Headers& headers, + const std::string& responseUrl) noexcept; + + void didReceiveNetworkData( + uint32_t requestId, + const std::string& responseType, + std::unique_ptr buf); + + int64_t didReceiveNetworkIncrementalData( + uint32_t requestId, + std::unique_ptr buf, + int64_t progress, + int64_t total); + + void didReceiveNetworkDataProgress( + uint32_t requestId, + int64_t bytesRead, + int64_t total); + + void didCompleteNetworkResponse( + uint32_t requestId, + const std::string& error, + bool timeoutError); + + std::unique_ptr httpClient_; + Requests requests_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp new file mode 100644 index 000000000000..5e800fba6297 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" +#include +#include +#include + +namespace facebook::react { +/* static */ bool ResourceLoader::isAbsolutePath(const std::string& path) { + return std::filesystem::path(path).is_absolute(); +} + +/* static */ bool ResourceLoader::isDirectory(const std::string& path) { + if (isAbsolutePath(path) && std::filesystem::is_directory(path)) { + return true; + } + + return isResourceDirectory(path); +} + +/* static */ bool ResourceLoader::isFile(const std::string& path) { + if (isAbsolutePath(path) && std::filesystem::exists(path) && + !std::filesystem::is_directory(path)) { + return true; + } + + return isResourceFile(path); +} + +/* static */ std::string ResourceLoader::getFileContents( + const std::string& path) { + if (isAbsolutePath(path)) { + std::ifstream file(path, std::ios::binary); + if (!file.good()) { + return getResourceFileContents(path); + } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + + return getResourceFileContents(path); +} + +/* static */ std::filesystem::path ResourceLoader::getCacheDirectory( + const std::string& path) { + auto root = getCacheRootPath() / CACHE_DIR; + if (!std::filesystem::exists(root)) { + try { + std::filesystem::create_directory(root); + } catch (...) { + LOG(ERROR) << "Failed to create root cache directory: " << root; + throw; + } + } + + if (path.empty()) { + return root; + } + + auto dir = root / path; + try { + std::filesystem::create_directories(dir); + return dir; + } catch (...) { + LOG(ERROR) << "Failed to create cache directory: " << dir; + throw; + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h new file mode 100644 index 000000000000..d4aa4f4c5606 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/ResourceLoader.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { +class ResourceLoader { + public: + static bool isDirectory(const std::string& path); + static bool isFile(const std::string& path); + static bool isAbsolutePath(const std::string& path); + static std::string getFileContents(const std::string& path); + static std::filesystem::path getCacheDirectory( + const std::string& path = std::string()); + + protected: + static bool isResourceDirectory(const std::string& path); + static bool isResourceFile(const std::string& path); + static std::string getResourceFileContents(const std::string& path); + + private: + static constexpr const auto CACHE_DIR = ".react-native-cxx-cache"; + static std::filesystem::path getCacheRootPath(); +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp new file mode 100644 index 000000000000..c2198d0008fb --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "WebSocketModule.h" + +#include + +namespace facebook::react { + +WebSocketModule::WebSocketModule( + std::shared_ptr jsInvoker, + WebSocketClientFactory webSocketClientFactory) + : NativeWebSocketModuleCxxSpec(jsInvoker), + webSocketClientFactory_(std::move(webSocketClientFactory)) {} + +WebSocketModule::~WebSocketModule() { + connections_.clear(); +} + +void WebSocketModule::connect( + jsi::Runtime& /*rt*/, + const std::string& url, + const std::optional>& /*protocols*/, + jsi::Object /*options*/, + int32_t socketID) { + auto webSocket = webSocketClientFactory_(); + connections_.emplace(socketID, std::move(webSocket)); + connections_[socketID]->setOnMessageCallback([weakThis = weak_from_this(), + socketID]( + const std::string& message) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + strongThis->emitDeviceEvent( + "websocketMessage", + [socketID, message = std::string(message)]( + jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty( + rt, "type", facebook::jsi::String::createFromAscii(rt, "string")); + arg.setProperty( + rt, "data", facebook::jsi::String::createFromUtf8(rt, message)); + args.emplace_back(rt, arg); + }); + }); + connections_[socketID]->setOnClosedCallback( + [weakThis = weak_from_this(), socketID](const std::string& /*reason*/) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + strongThis->emitDeviceEvent( + "websocketClosed", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty(rt, "code", jsi::Value(0)); + arg.setProperty( + rt, + "reason", + facebook::jsi::String::createFromAscii(rt, "closed")); + args.emplace_back(rt, arg); + }); + }); + + connections_[socketID]->connect( + url, + [weakThis = weak_from_this(), socketID]( + bool success, const std::optional& /*message*/) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + if (success) { + strongThis->emitDeviceEvent( + "websocketOpen", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + args.emplace_back(rt, arg); + }); + } else { + strongThis->emitDeviceEvent( + "websocketFailed", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty( + rt, + "message", + facebook::jsi::String::createFromAscii( + rt, "Could not connect to websocket.")); + args.emplace_back(rt, arg); + }); + } + }); +} + +void WebSocketModule::send( + jsi::Runtime& /*rt*/, + const std::string& message, + int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to send data to a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->send(message); +} + +void WebSocketModule::sendBinary( + jsi::Runtime& rt, + const std::string& base64String, + int32_t forSocketID) { + send(rt, base64String, forSocketID); +} + +void WebSocketModule::ping(jsi::Runtime& /*rt*/, int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to ping a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->ping(); +} + +void WebSocketModule::close( + jsi::Runtime& /*rt*/, + int32_t /*code*/, + const std::string& reason, + int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to close a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->close(reason); + connections_.erase(it); +} + +void WebSocketModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& /*eventName*/) {} + +void WebSocketModule::removeListeners(jsi::Runtime& /*rt*/, int32_t /*count*/) { +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h new file mode 100644 index 000000000000..f12ca1af328b --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/WebSocketModule.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient; + +class WebSocketModule : public NativeWebSocketModuleCxxSpec, + public std::enable_shared_from_this { + public: + WebSocketModule( + std::shared_ptr jsInvoker, + WebSocketClientFactory webSocketClientFactory); + ~WebSocketModule() override; + WebSocketModule(const WebSocketModule& other) = delete; + WebSocketModule& operator=(WebSocketModule& other) = delete; + WebSocketModule(WebSocketModule&& other) = delete; + WebSocketModule& operator=(WebSocketModule&& other) = delete; + + void connect( + jsi::Runtime& rt, + const std::string& url, + const std::optional>& protocols, + jsi::Object options, + int32_t socketID); + + void send(jsi::Runtime& rt, const std::string& message, int32_t socketID); + + void sendBinary( + jsi::Runtime& rt, + const std::string& base64String, + int32_t socketID); + + void ping(jsi::Runtime& rt, int32_t socketID); + + void close( + jsi::Runtime& rt, + int32_t code, + const std::string& reason, + int32_t socketID); + + void addListener(jsi::Runtime& rt, const std::string& eventName); + + void removeListeners(jsi::Runtime& rt, int32_t count); + + private: + WebSocketClientFactory webSocketClientFactory_; + std::unordered_map> connections_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp new file mode 100644 index 000000000000..8fd3a182d46b --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "AssetManagerHelpers.h" + +#include +#include + +namespace facebook::react { + +jobject getAssetManagerObject(JNIEnv* env) { + auto contextObject = getApplication(env); + auto contextClass = env->GetObjectClass(contextObject); + auto getAssetsMethod = env->GetMethodID( + contextClass, "getAssets", "()Landroid/content/res/AssetManager;"); + return env->CallObjectMethod(contextObject, getAssetsMethod); +} + +AAssetManager* getJavaAssetManager() { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto AssetManagerObject = getAssetManagerObject(env); + return AAssetManager_fromJava(env, AssetManagerObject); +} + +bool isDirectoryNotEmpty(const std::string& path) { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto AssetManagerObject = getAssetManagerObject(env); + auto listMethodID = env->GetMethodID( + env->GetObjectClass(AssetManagerObject), + "list", + "(Ljava/lang/String;)[Ljava/lang/String;"); + + auto pathString = env->NewStringUTF(path.c_str()); + auto filesObject = static_cast( + env->CallObjectMethod(AssetManagerObject, listMethodID, pathString)); + env->DeleteLocalRef(pathString); + + auto length = env->GetArrayLength(filesObject); + for (int i = 0; i < length; i++) { + auto jstr = + static_cast(env->GetObjectArrayElement(filesObject, i)); + + const char* filename = env->GetStringUTFChars(jstr, nullptr); + + if (filename != nullptr) { + env->ReleaseStringUTFChars(jstr, filename); + env->DeleteLocalRef(jstr); + return true; + } + + env->DeleteLocalRef(jstr); + } + return false; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h new file mode 100644 index 000000000000..234cbd43393d --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +AAssetManager* getJavaAssetManager(); + +bool isDirectoryNotEmpty(const std::string& path); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp b/packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp new file mode 100644 index 000000000000..c6dedb6b4686 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" +#include "AssetManagerHelpers.h" + +#include +#include +#include +#include + +namespace facebook::react { + +namespace { +AAssetManager* assetManager_ = nullptr; + +AAssetManager* getAssetManager() { + if (assetManager_ == nullptr) { + assetManager_ = getJavaAssetManager(); + } + + return assetManager_; +} +} // namespace + +bool ResourceLoader::isResourceDirectory(const std::string& path) { + auto assetDir = AAssetManager_openDir(getAssetManager(), path.c_str()); + if (assetDir == nullptr) { + return false; + } + + bool exists = AAssetDir_getNextFileName(assetDir) != nullptr; + AAssetDir_close(assetDir); + if (exists) { + return true; + } + return isDirectoryNotEmpty(path); +} + +bool ResourceLoader::isResourceFile(const std::string& path) { + auto asset = AAssetManager_open( + getAssetManager(), path.c_str(), AASSET_MODE_STREAMING); + if (asset == nullptr) { + return false; + } + + AAsset_close(asset); + return true; +} + +std::string ResourceLoader::getResourceFileContents(const std::string& path) { + auto asset = AAssetManager_open( + getAssetManager(), path.c_str(), AASSET_MODE_STREAMING); + if (asset == nullptr) { + throw std::runtime_error("File not found " + path); + } + + std::string result( + (const char*)AAsset_getBuffer(asset), (size_t)AAsset_getLength(asset)); + AAsset_close(asset); + return result; +} + +std::filesystem::path ResourceLoader::getCacheRootPath() { + return getContext()->getCacheDir()->getAbsolutePath(); +} +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp b/packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp new file mode 100644 index 000000000000..6d74f5bbb5a5 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" + +#include +#include +#include + +namespace facebook::react { + +bool ResourceLoader::isResourceDirectory(const std::string& path) { + return std::filesystem::is_directory(path); +} + +bool ResourceLoader::isResourceFile(const std::string& path) { + return std::filesystem::exists(path) && !std::filesystem::is_directory(path); +} + +std::string ResourceLoader::getResourceFileContents(const std::string& path) { + std::ifstream file(path, std::ios::binary); + if (!file.good()) { + throw std::runtime_error("File not found " + path); + } + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} + +std::filesystem::path ResourceLoader::getCacheRootPath() { + return std::filesystem::temp_directory_path(); +} +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp b/packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp new file mode 100644 index 000000000000..62b113d09893 --- /dev/null +++ b/packages/react-native/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class TestCallInvoker : public CallInvoker { + public: + void invokeAsync(CallFunc&& fn) noexcept override { + queue_.push_back(std::move(fn)); + } + + void invokeSync(CallFunc&& /*func*/) override { + FAIL() << "JSCallInvoker does not support invokeSync()"; + } + + private: + std::list queue_; +}; + +class NetworkingModuleTests : public testing::Test { + protected: + void SetUp() override { + rt_ = facebook::hermes::makeHermesRuntime(); + jsInvoker_ = std::make_shared(); + } + + static void verifyFormData( + const http::FormDataField& formData, + const std::string& fieldName, + const std::string& string, + std::vector headerValues) { + EXPECT_EQ(formData.fieldName, fieldName); + EXPECT_EQ(formData.string, string); + for (int i = 0; i * 2 < headerValues.size(); ++i) { + EXPECT_EQ(formData.headers[i].first, headerValues[i * 2]); + EXPECT_EQ(formData.headers[i].second, headerValues[i * 2 + 1]); + } + } + + std::unique_ptr rt_; + std::shared_ptr jsInvoker_; +}; + +// Test parsing a body with form data +TEST_F(NetworkingModuleTests, formDataTest) { + auto dynamic = folly::parseJson( + R"({ + "formData":[ + { + "fieldName":"field1", + "headers":[["header1","form-data; name=\"header1\""]], + "string":"string1" + }, + { + "fieldName":"field2", + "headers":[ + ["header1","form-data; name=\"header1\""], + ["header2","form-data; name=\"header2\""] + ], + "string":"string2" + } + ]})"); + auto jsiValue = jsi::valueFromDynamic(*rt_, dynamic); + auto body = bridging::fromJs(*rt_, jsiValue, jsInvoker_); + + EXPECT_TRUE(body.formData.has_value()); + + EXPECT_FALSE(body.base64.has_value()); + EXPECT_FALSE(body.blob.has_value()); + EXPECT_FALSE(body.string.has_value()); + + auto formData = body.formData.value(); + EXPECT_EQ(formData.size(), 2); + verifyFormData( + formData[0], + "field1", + "string1", + {"header1", "form-data; name=\"header1\""}); + verifyFormData( + formData[1], + "field2", + "string2", + {"header1", + "form-data; name=\"header1\"", + "header2", + "form-data; name=\"header2\""}); +} + +// Test parsing a body with string data +TEST_F(NetworkingModuleTests, stringDataTest) { + auto dynamic = folly::parseJson( + R"({ + "string": "testString" + })"); + auto jsiValue = jsi::valueFromDynamic(*rt_, dynamic); + auto body = bridging::fromJs(*rt_, jsiValue, jsInvoker_); + + EXPECT_TRUE(body.string.has_value()); + + EXPECT_FALSE(body.base64.has_value()); + EXPECT_FALSE(body.blob.has_value()); + EXPECT_FALSE(body.formData.has_value()); + + auto stringData = body.string.value(); + EXPECT_EQ(stringData, "testString"); +} + +} // namespace facebook::react