diff --git a/.changeset/afraid-buttons-go.md b/.changeset/afraid-buttons-go.md new file mode 100644 index 000000000..65b7d4249 --- /dev/null +++ b/.changeset/afraid-buttons-go.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": minor +--- + +Allow customizing the native HTTP client used to download remote scripts: `RemoteScriptLoader.okHttpClientFactory` on Android and `ScriptManager.urlSessionFactory` on iOS (for SSL pinning, interceptors, custom headers, timeouts, etc.) diff --git a/packages/repack/android/src/main/java/com/callstack/repack/RemoteScriptLoader.kt b/packages/repack/android/src/main/java/com/callstack/repack/RemoteScriptLoader.kt index 70c5e2b09..7228a8d3a 100644 --- a/packages/repack/android/src/main/java/com/callstack/repack/RemoteScriptLoader.kt +++ b/packages/repack/android/src/main/java/com/callstack/repack/RemoteScriptLoader.kt @@ -15,7 +15,19 @@ import java.util.concurrent.TimeUnit class RemoteScriptLoader(val reactContext: ReactContext, private val nativeLoader: NativeScriptLoader) { private val scriptsDirName = "scripts" - private val client = OkHttpClient() + private val client: OkHttpClient by lazy(okHttpClientFactory) + + companion object { + /** + * Factory used to create the [OkHttpClient] for downloading remote scripts. + * + * Set this before any remote script is loaded (e.g. in your Application's + * onCreate) to provide a custom client - for SSL pinning, interceptors, + * proxies, timeouts, etc. Defaults to a plain `OkHttpClient()`. This is the + * Android counterpart of `ScriptManager.urlSessionFactory` on iOS. + */ + var okHttpClientFactory: () -> OkHttpClient = { OkHttpClient() } + } private fun getScriptFilePath(scriptUniqueId: String): String { return "${scriptsDirName}/$scriptUniqueId.script.bundle" diff --git a/packages/repack/ios/ScriptManager.h b/packages/repack/ios/ScriptManager.h index 256abb549..7bd197f09 100644 --- a/packages/repack/ios/ScriptManager.h +++ b/packages/repack/ios/ScriptManager.h @@ -8,4 +8,15 @@ @interface ScriptManager : NSObject #endif +/** + * Factory used to create the `NSURLSession` for downloading remote scripts. + * + * Set this before any remote script is loaded (e.g. in your AppDelegate) to + * provide a custom session - for SSL pinning, custom headers, proxies, timeouts, + * etc. Defaults to `[NSURLSession sharedSession]`. Assign `nil` to restore the + * default. This is the iOS counterpart of `RemoteScriptLoader.okHttpClientFactory` + * on Android. + */ +@property (class, nonatomic, copy, null_resettable) NSURLSession * (^urlSessionFactory)(void); + @end diff --git a/packages/repack/ios/ScriptManager.mm b/packages/repack/ios/ScriptManager.mm index 6f0297eef..0a2909f6c 100644 --- a/packages/repack/ios/ScriptManager.mm +++ b/packages/repack/ios/ScriptManager.mm @@ -23,12 +23,42 @@ @interface RCTBridge (RCTTurboModule) @end #endif +static NSURLSession * (^_urlSessionFactory)(void) = nil; +static NSURLSession *_cachedURLSession = nil; + @implementation ScriptManager RCT_EXPORT_MODULE() @synthesize bridge = _bridge; ++ (NSURLSession * (^)(void))urlSessionFactory +{ + if (_urlSessionFactory == nil) { + _urlSessionFactory = ^NSURLSession * { + return [NSURLSession sharedSession]; + }; + } + return _urlSessionFactory; +} + ++ (void)setUrlSessionFactory:(NSURLSession * (^)(void))urlSessionFactory +{ + _urlSessionFactory = [urlSessionFactory copy]; + // Invalidate the cached session so the next download picks up the new factory. + _cachedURLSession = nil; +} + +// Lazily build (and cache) the session from the factory, mirroring Android's +// `OkHttpClient by lazy(okHttpClientFactory)`. +- (NSURLSession *)urlSession +{ + if (_cachedURLSession == nil) { + _cachedURLSession = ScriptManager.urlSessionFactory(); + } + return _cachedURLSession; +} + #ifdef RCT_NEW_ARCH_ENABLED RCT_EXPORT_METHOD(loadScript : (nonnull NSString *)scriptId scriptConfig @@ -235,7 +265,7 @@ - (void)downloadAndCache:(ScriptConfig *)config completionHandler:(void (^)(NSEr [request setValue:@"text/plain" forHTTPHeaderField:@"content-type"]; } - NSURLSessionDataTask *task = [[NSURLSession sharedSession] + NSURLSessionDataTask *task = [self.urlSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; diff --git a/website/src/latest/api/runtime/script-manager.md b/website/src/latest/api/runtime/script-manager.md index 4121a1fa7..0c840aaa2 100644 --- a/website/src/latest/api/runtime/script-manager.md +++ b/website/src/latest/api/runtime/script-manager.md @@ -305,6 +305,83 @@ ScriptManager.shared.hooks.errorLoad(async (args) => { }); ``` +## Customizing the native HTTP client + +Remote scripts are downloaded by the native side of Re.Pack - `OkHttpClient` on Android and `NSURLSession` on iOS. By default a plain client is used, but you can provide your own to customize networking behavior such as **SSL/certificate pinning**, **interceptors**, **proxies**, **custom headers**, or **timeouts**. + +The factory must be set **before any remote script is loaded** - the earliest app lifecycle hook is the safest place (Android `Application.onCreate`, iOS `application:didFinishLaunchingWithOptions:`). The client is created lazily and reused for all subsequent downloads. + +:::warning Native customization only + +This applies to the native HTTP client used for downloading scripts. Per-request options exposed to JavaScript (such as `headers`, `method`, `body`, `timeout` and `retry`) are still configured through the [resolver](#addresolver). + +::: + +### Android + +Assign a factory to `RemoteScriptLoader.okHttpClientFactory`: + +```kotlin +// MainApplication.kt +import com.callstack.repack.RemoteScriptLoader +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit + +class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + + RemoteScriptLoader.okHttpClientFactory = { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .addInterceptor(MyAuthInterceptor()) + // .certificatePinner(...) for SSL pinning + .build() + } + // ... + } +} +``` + +### iOS + +Assign a factory to `ScriptManager.urlSessionFactory`. Assign `nil` to restore the default `[NSURLSession sharedSession]`. + +```swift +// AppDelegate.swift +import callstack_repack + +func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? +) -> Bool { + ScriptManager.urlSessionFactory = { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = 30 + configuration.httpAdditionalHeaders = ["X-Custom-Header": "value"] + // Pass a delegate for SSL pinning if needed. + return URLSession(configuration: configuration) + } + // ... +} +``` + +```objc +// AppDelegate.mm +#import // or "ScriptManager.h" + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + ScriptManager.urlSessionFactory = ^NSURLSession * { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + configuration.timeoutIntervalForRequest = 30; + return [NSURLSession sessionWithConfiguration:configuration]; + }; + // ... +} +``` + ## Related - [Script](/api/runtime/script) - Utility class for generating script URLs used with resolvers