[ReactNative] Allow uploading native files (e.g. photos) and FormData via XMLHttpRequest
This commit is contained in:
parent
f590a8b15b
commit
f4bf80f3ea
|
@ -82,8 +82,8 @@ static NSString *RCTCacheKeyForURL(NSURL *url)
|
||||||
RCTImageDownloader *strongSelf = weakSelf;
|
RCTImageDownloader *strongSelf = weakSelf;
|
||||||
NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
|
NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
|
||||||
[strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
|
[strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
|
||||||
for (RCTCachedDataDownloadBlock cacheDownloadBlock in blocks) {
|
for (RCTCachedDataDownloadBlock downloadBlock in blocks) {
|
||||||
cacheDownloadBlock(cached, data, error);
|
downloadBlock(cached, data, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,4 +23,6 @@
|
||||||
+ (void)loadImageWithTag:(NSString *)tag
|
+ (void)loadImageWithTag:(NSString *)tag
|
||||||
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;
|
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;
|
||||||
|
|
||||||
|
+ (BOOL)isSystemImageURI:(NSString *)uri;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#import "RCTGIFImage.h"
|
#import "RCTGIFImage.h"
|
||||||
#import "RCTImageDownloader.h"
|
#import "RCTImageDownloader.h"
|
||||||
#import "RCTLog.h"
|
#import "RCTLog.h"
|
||||||
|
#import "RCTUtils.h"
|
||||||
|
|
||||||
static dispatch_queue_t RCTImageLoaderQueue(void)
|
static dispatch_queue_t RCTImageLoaderQueue(void)
|
||||||
{
|
{
|
||||||
|
@ -31,24 +32,6 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
|
||||||
return queue;
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSError *RCTErrorWithMessage(NSString *message)
|
|
||||||
{
|
|
||||||
NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message};
|
|
||||||
NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSError *error, UIImage *image)
|
|
||||||
{
|
|
||||||
if ([NSThread isMainThread]) {
|
|
||||||
callback(error, image);
|
|
||||||
} else {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
callback(error, image);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation RCTImageLoader
|
@implementation RCTImageLoader
|
||||||
|
|
||||||
+ (ALAssetsLibrary *)assetsLibrary
|
+ (ALAssetsLibrary *)assetsLibrary
|
||||||
|
@ -154,4 +137,11 @@ static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSEr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (BOOL)isSystemImageURI:(NSString *)uri
|
||||||
|
{
|
||||||
|
return uri != nil && (
|
||||||
|
[uri hasPrefix:@"assets-library"] ||
|
||||||
|
[uri hasPrefix:@"ph://"]);
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -102,6 +102,7 @@ function setUpXHR() {
|
||||||
// The native XMLHttpRequest in Chrome dev tools is CORS aware and won't
|
// The native XMLHttpRequest in Chrome dev tools is CORS aware and won't
|
||||||
// let you fetch anything from the internet
|
// let you fetch anything from the internet
|
||||||
GLOBAL.XMLHttpRequest = require('XMLHttpRequest');
|
GLOBAL.XMLHttpRequest = require('XMLHttpRequest');
|
||||||
|
GLOBAL.FormData = require('FormData');
|
||||||
|
|
||||||
var fetchPolyfill = require('fetch');
|
var fetchPolyfill = require('fetch');
|
||||||
GLOBAL.fetch = fetchPolyfill.fetch;
|
GLOBAL.fetch = fetchPolyfill.fetch;
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule FormData
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
type FormDataValue = any;
|
||||||
|
type FormDataPart = [string, FormDataValue];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polyfill for XMLHttpRequest2 FormData API, allowing multipart POST requests
|
||||||
|
* with mixed data (string, native files) to be submitted via XMLHttpRequest.
|
||||||
|
*/
|
||||||
|
class FormData {
|
||||||
|
_parts: Array<FormDataPart>;
|
||||||
|
_partsByKey: {[key: string]: FormDataPart};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._parts = [];
|
||||||
|
this._partsByKey = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
append(key: string, value: FormDataValue) {
|
||||||
|
var parts = this._partsByKey[key];
|
||||||
|
if (parts) {
|
||||||
|
// It's a bit unclear what the behaviour should be in this case.
|
||||||
|
// The XMLHttpRequest spec doesn't specify it, while MDN says that
|
||||||
|
// the any new values should appended to existing values. We're not
|
||||||
|
// doing that for now -- it's tedious and doesn't seem worth the effort.
|
||||||
|
parts[1] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parts = [key, value];
|
||||||
|
this._parts.push(parts);
|
||||||
|
this._partsByKey[key] = parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParts(): Array<FormDataValue> {
|
||||||
|
return this._parts.map(([name, value]) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return {
|
||||||
|
string: value,
|
||||||
|
headers: {
|
||||||
|
'content-disposition': 'form-data; name="' + name + '"',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var contentDisposition = 'form-data; name="' + name + '"';
|
||||||
|
if (typeof value.name === 'string') {
|
||||||
|
contentDisposition += '; filename="' + value.name + '"';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
headers: {'content-disposition': contentDisposition},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FormData;
|
|
@ -11,19 +11,13 @@
|
||||||
|
|
||||||
#import "RCTAssert.h"
|
#import "RCTAssert.h"
|
||||||
#import "RCTConvert.h"
|
#import "RCTConvert.h"
|
||||||
|
#import "RCTDataQuery.h"
|
||||||
#import "RCTEventDispatcher.h"
|
#import "RCTEventDispatcher.h"
|
||||||
|
#import "RCTHTTPQueryExecutor.h"
|
||||||
#import "RCTLog.h"
|
#import "RCTLog.h"
|
||||||
#import "RCTUtils.h"
|
#import "RCTUtils.h"
|
||||||
|
|
||||||
@interface RCTDataManager () <NSURLSessionDataDelegate>
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation RCTDataManager
|
@implementation RCTDataManager
|
||||||
{
|
|
||||||
NSURLSession *_session;
|
|
||||||
NSOperationQueue *_callbackQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@synthesize bridge = _bridge;
|
@synthesize bridge = _bridge;
|
||||||
|
|
||||||
|
@ -38,119 +32,23 @@ RCT_EXPORT_METHOD(queryData:(NSString *)queryType
|
||||||
sendIncrementalUpdates:(BOOL)incrementalUpdates
|
sendIncrementalUpdates:(BOOL)incrementalUpdates
|
||||||
responseSender:(RCTResponseSenderBlock)responseSender)
|
responseSender:(RCTResponseSenderBlock)responseSender)
|
||||||
{
|
{
|
||||||
|
id<RCTDataQueryExecutor> executor = nil;
|
||||||
if ([queryType isEqualToString:@"http"]) {
|
if ([queryType isEqualToString:@"http"]) {
|
||||||
|
executor = [RCTHTTPQueryExecutor sharedInstance];
|
||||||
// Build request
|
|
||||||
NSURL *URL = [RCTConvert NSURL:query[@"url"]];
|
|
||||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
|
|
||||||
request.HTTPMethod = [RCTConvert NSString:query[@"method"]] ?: @"GET";
|
|
||||||
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
|
|
||||||
request.HTTPBody = [RCTConvert NSData:query[@"data"]];
|
|
||||||
|
|
||||||
// Create session if one doesn't already exist
|
|
||||||
if (!_session) {
|
|
||||||
_callbackQueue = [[NSOperationQueue alloc] init];
|
|
||||||
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
||||||
_session = [NSURLSession sessionWithConfiguration:configuration
|
|
||||||
delegate:self
|
|
||||||
delegateQueue:_callbackQueue];
|
|
||||||
}
|
|
||||||
|
|
||||||
__block NSURLSessionDataTask *task;
|
|
||||||
if (incrementalUpdates) {
|
|
||||||
task = [_session dataTaskWithRequest:request];
|
|
||||||
} else {
|
|
||||||
task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
||||||
RCTSendResponseEvent(_bridge, task);
|
|
||||||
if (!error) {
|
|
||||||
RCTSendDataEvent(_bridge, task, data);
|
|
||||||
}
|
|
||||||
RCTSendCompletionEvent(_bridge, task, error);
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build data task
|
|
||||||
responseSender(@[@(task.taskIdentifier)]);
|
|
||||||
[task resume];
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
RCTLogError(@"unsupported query type %@", queryType);
|
RCTLogError(@"unsupported query type %@", queryType);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - URLSession delegate
|
|
||||||
|
|
||||||
- (void)URLSession:(NSURLSession *)session
|
|
||||||
dataTask:(NSURLSessionDataTask *)task
|
|
||||||
didReceiveResponse:(NSURLResponse *)response
|
|
||||||
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
|
|
||||||
{
|
|
||||||
RCTSendResponseEvent(_bridge, task);
|
|
||||||
completionHandler(NSURLSessionResponseAllow);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)URLSession:(NSURLSession *)session
|
|
||||||
dataTask:(NSURLSessionDataTask *)task
|
|
||||||
didReceiveData:(NSData *)data
|
|
||||||
{
|
|
||||||
RCTSendDataEvent(_bridge, task, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
|
|
||||||
{
|
|
||||||
RCTSendCompletionEvent(_bridge, task, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Build responses
|
|
||||||
|
|
||||||
static void RCTSendResponseEvent(RCTBridge *bridge, NSURLSessionTask *task)
|
|
||||||
{
|
|
||||||
NSURLResponse *response = task.response;
|
|
||||||
NSHTTPURLResponse *httpResponse = nil;
|
|
||||||
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
||||||
// Might be a local file request
|
|
||||||
httpResponse = (NSHTTPURLResponse *)response;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSArray *responseJSON = @[@(task.taskIdentifier),
|
|
||||||
@(httpResponse.statusCode ?: 200),
|
|
||||||
httpResponse.allHeaderFields ?: @{},
|
|
||||||
];
|
|
||||||
|
|
||||||
[bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse"
|
|
||||||
body:responseJSON];
|
|
||||||
}
|
|
||||||
|
|
||||||
static void RCTSendDataEvent(RCTBridge *bridge, NSURLSessionDataTask *task, NSData *data)
|
|
||||||
{
|
|
||||||
// Get text encoding
|
|
||||||
NSURLResponse *response = task.response;
|
|
||||||
NSStringEncoding encoding = NSUTF8StringEncoding;
|
|
||||||
if (response.textEncodingName) {
|
|
||||||
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
|
|
||||||
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding];
|
|
||||||
if (!responseText && data.length) {
|
|
||||||
RCTLogError(@"Received data was invalid.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSArray *responseJSON = @[@(task.taskIdentifier), responseText ?: @""];
|
RCTAssert(executor != nil, @"executor must be defined");
|
||||||
[bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData"
|
|
||||||
body:responseJSON];
|
|
||||||
}
|
|
||||||
|
|
||||||
static void RCTSendCompletionEvent(RCTBridge *bridge, NSURLSessionTask *task, NSError *error)
|
if ([executor respondsToSelector:@selector(setBridge:)]) {
|
||||||
{
|
executor.bridge = _bridge;
|
||||||
NSArray *responseJSON = @[@(task.taskIdentifier),
|
}
|
||||||
error.localizedDescription ?: [NSNull null],
|
if ([executor respondsToSelector:@selector(setSendIncrementalUpdates:)]) {
|
||||||
];
|
executor.sendIncrementalUpdates = incrementalUpdates;
|
||||||
|
}
|
||||||
[bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
|
[executor addQuery:query responseSender:responseSender];
|
||||||
body:responseJSON];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "RCTBridgeModule.h"
|
||||||
|
|
||||||
|
@protocol RCTDataQueryExecutor <NSObject>
|
||||||
|
|
||||||
|
- (void)addQuery:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender;
|
||||||
|
|
||||||
|
@optional
|
||||||
|
|
||||||
|
@property (nonatomic, weak) RCTBridge *bridge;
|
||||||
|
@property (nonatomic, assign) BOOL sendIncrementalUpdates;
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#import "RCTDataQuery.h"
|
||||||
|
|
||||||
|
@interface RCTHTTPQueryExecutor : NSObject <RCTDataQueryExecutor>
|
||||||
|
|
||||||
|
+ (instancetype)sharedInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the 'data' part of an HTTP query.
|
||||||
|
*
|
||||||
|
* 'data' can be a JSON value of the following forms:
|
||||||
|
*
|
||||||
|
* - {"string": "..."}: a simple JS string that will be UTF-8 encoded and sent as the body
|
||||||
|
*
|
||||||
|
* - {"uri": "some-uri://..."}: reference to a system resource, e.g. an image in the asset library
|
||||||
|
*
|
||||||
|
* - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request
|
||||||
|
*
|
||||||
|
* If successful, the callback be called with a result dictionary containing the following (optional) keys:
|
||||||
|
*
|
||||||
|
* - @"body" (NSData): the body of the request
|
||||||
|
*
|
||||||
|
* - @"contentType" (NSString): the content type header of the request
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
+ (void)processDataForHTTPQuery:(NSDictionary *)data
|
||||||
|
callback:(void (^)(NSError *error, NSDictionary *result))callback;
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,314 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "RCTHTTPQueryExecutor.h"
|
||||||
|
|
||||||
|
#import "RCTAssert.h"
|
||||||
|
#import "RCTConvert.h"
|
||||||
|
#import "RCTEventDispatcher.h"
|
||||||
|
#import "RCTImageLoader.h"
|
||||||
|
#import "RCTLog.h"
|
||||||
|
#import "RCTUtils.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to convert FormData payloads into multipart/formdata requests.
|
||||||
|
*/
|
||||||
|
@interface RCTHTTPFormDataHelper : NSObject
|
||||||
|
|
||||||
|
- (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))callback;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RCTHTTPFormDataHelper
|
||||||
|
{
|
||||||
|
NSMutableArray *parts;
|
||||||
|
NSMutableData *multipartBody;
|
||||||
|
RCTResultOrErrorBlock callback;
|
||||||
|
NSString *boundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))cb
|
||||||
|
{
|
||||||
|
if (![formData count]) {
|
||||||
|
RCTDispatchCallbackOnMainQueue(cb, nil, nil);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parts = [formData mutableCopy];
|
||||||
|
callback = cb;
|
||||||
|
multipartBody = [[NSMutableData alloc] init];
|
||||||
|
boundary = [self generateBoundary];
|
||||||
|
|
||||||
|
NSDictionary *currentPart = [parts objectAtIndex: 0];
|
||||||
|
[RCTHTTPQueryExecutor processDataForHTTPQuery:currentPart callback:^(NSError *e, NSDictionary *r) {
|
||||||
|
[self handleResult:r error:e];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)generateBoundary
|
||||||
|
{
|
||||||
|
NSString *const boundaryChars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./";
|
||||||
|
const NSUInteger boundaryLength = 70;
|
||||||
|
|
||||||
|
NSMutableString *output = [NSMutableString stringWithCapacity:boundaryLength];
|
||||||
|
NSUInteger numchars = [boundaryChars length];
|
||||||
|
for (NSUInteger i = 0; i < boundaryLength; i++) {
|
||||||
|
[output appendFormat:@"%C", [boundaryChars characterAtIndex:arc4random_uniform((u_int32_t)numchars)]];
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleResult:(NSDictionary *)result error:(NSError *)error
|
||||||
|
{
|
||||||
|
if (error) {
|
||||||
|
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSDictionary *currentPart = parts[0];
|
||||||
|
[parts removeObjectAtIndex:0];
|
||||||
|
|
||||||
|
// Start with boundary.
|
||||||
|
[multipartBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary]
|
||||||
|
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||||
|
|
||||||
|
// Print headers.
|
||||||
|
NSMutableDictionary *headers = [(NSDictionary*)currentPart[@"headers"] mutableCopy];
|
||||||
|
NSString *partContentType = result[@"contentType"];
|
||||||
|
if (partContentType != nil) {
|
||||||
|
[headers setObject:partContentType forKey:@"content-type"];
|
||||||
|
}
|
||||||
|
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) {
|
||||||
|
[multipartBody appendData:[[NSString stringWithFormat:@"%@: %@\r\n", parameterKey, parameterValue]
|
||||||
|
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Add the body.
|
||||||
|
[multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||||
|
[multipartBody appendData:result[@"body"]];
|
||||||
|
[multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||||
|
|
||||||
|
if ([parts count]) {
|
||||||
|
NSDictionary *nextPart = [parts objectAtIndex: 0];
|
||||||
|
[RCTHTTPQueryExecutor processDataForHTTPQuery:nextPart callback:^(NSError *e, NSDictionary *r) {
|
||||||
|
[self handleResult:r error:e];
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've processed the last item. Finish and return.
|
||||||
|
[multipartBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary]
|
||||||
|
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||||
|
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=\"%@\"", boundary];
|
||||||
|
callback(nil, @{@"body": multipartBody, @"contentType": contentType});
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface RCTHTTPQueryExecutor () <NSURLSessionDataDelegate>
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RCTHTTPQueryExecutor
|
||||||
|
{
|
||||||
|
NSURLSession *_session;
|
||||||
|
NSOperationQueue *_callbackQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@synthesize bridge = _bridge;
|
||||||
|
@synthesize sendIncrementalUpdates = _sendIncrementalUpdates;
|
||||||
|
|
||||||
|
+ (instancetype)sharedInstance
|
||||||
|
{
|
||||||
|
static RCTHTTPQueryExecutor *_sharedInstance = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
_sharedInstance = [[RCTHTTPQueryExecutor alloc] init];
|
||||||
|
});
|
||||||
|
return _sharedInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)addQuery:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender
|
||||||
|
{
|
||||||
|
[self makeRequest:query responseSender:responseSender];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)makeRequest:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender
|
||||||
|
{
|
||||||
|
// Build request
|
||||||
|
NSURL *URL = [RCTConvert NSURL:query[@"url"]];
|
||||||
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
|
||||||
|
request.HTTPMethod = [RCTConvert NSString:query[@"method"]] ?: @"GET";
|
||||||
|
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
|
||||||
|
|
||||||
|
NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]];
|
||||||
|
|
||||||
|
[[self class] processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) {
|
||||||
|
if (error != nil) {
|
||||||
|
RCTLogError(@"Error processing request body: %@", error);
|
||||||
|
// Ideally we'd circle back to JS here and notify an error/abort on the request.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request.HTTPBody = result[@"body"];
|
||||||
|
NSString *contentType = result[@"contentType"];
|
||||||
|
if (contentType != nil) {
|
||||||
|
[request setValue:contentType forHTTPHeaderField:@"content-type"];
|
||||||
|
}
|
||||||
|
[self sendRequest:request responseSender:responseSender];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)processURIDataForHTTPQuery:(NSString *)uri callback:(void (^)(NSError *error, NSDictionary *result))callback
|
||||||
|
{
|
||||||
|
if ([RCTImageLoader isSystemImageURI:uri]) {
|
||||||
|
[RCTImageLoader loadImageWithTag:(NSString *)uri callback:^(NSError *error, UIImage *image) {
|
||||||
|
if (error) {
|
||||||
|
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
|
||||||
|
RCTDispatchCallbackOnMainQueue(callback, nil, @{@"body": imageData, @"contentType": @"image/jpeg"});
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *errorText = [NSString stringWithFormat:@"Cannot resolve URI: %@", uri];
|
||||||
|
NSError *error = RCTErrorWithMessage(errorText);
|
||||||
|
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback
|
||||||
|
{
|
||||||
|
if (data == nil) {
|
||||||
|
RCTDispatchCallbackOnMainQueue(callback, nil, nil);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSData *body = [RCTConvert NSData:data[@"string"]];
|
||||||
|
if (body != nil) {
|
||||||
|
RCTDispatchCallbackOnMainQueue(callback, nil, @{@"body": body});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *uri = [RCTConvert NSString:data[@"uri"]];
|
||||||
|
if (uri != nil) {
|
||||||
|
[RCTHTTPQueryExecutor processURIDataForHTTPQuery:uri callback:callback];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:data[@"formData"]];
|
||||||
|
if (formData != nil) {
|
||||||
|
RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init];
|
||||||
|
[formDataHelper process:formData callback:callback];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Nothing in the data payload, at least nothing we could understand anyway.
|
||||||
|
// Ignore and treat it as if it were null.
|
||||||
|
RCTDispatchCallbackOnMainQueue(callback, nil, nil);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)sendRequest:(NSURLRequest *)request responseSender:(RCTResponseSenderBlock)responseSender
|
||||||
|
{
|
||||||
|
// Create session if one doesn't already exist
|
||||||
|
if (!_session) {
|
||||||
|
_callbackQueue = [[NSOperationQueue alloc] init];
|
||||||
|
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||||
|
_session = [NSURLSession sessionWithConfiguration:configuration
|
||||||
|
delegate:self
|
||||||
|
delegateQueue:_callbackQueue];
|
||||||
|
}
|
||||||
|
|
||||||
|
__block NSURLSessionDataTask *task;
|
||||||
|
if (_sendIncrementalUpdates) {
|
||||||
|
task = [_session dataTaskWithRequest:request];
|
||||||
|
} else {
|
||||||
|
task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||||
|
RCTSendResponseEvent(_bridge, task);
|
||||||
|
if (!error) {
|
||||||
|
RCTSendDataEvent(_bridge, task, data);
|
||||||
|
}
|
||||||
|
RCTSendCompletionEvent(_bridge, task, error);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build data task
|
||||||
|
responseSender(@[@(task.taskIdentifier)]);
|
||||||
|
[task resume];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - URLSession delegate
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
dataTask:(NSURLSessionDataTask *)task
|
||||||
|
didReceiveResponse:(NSURLResponse *)response
|
||||||
|
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
|
||||||
|
{
|
||||||
|
RCTSendResponseEvent(_bridge, task);
|
||||||
|
completionHandler(NSURLSessionResponseAllow);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session
|
||||||
|
dataTask:(NSURLSessionDataTask *)task
|
||||||
|
didReceiveData:(NSData *)data
|
||||||
|
{
|
||||||
|
RCTSendDataEvent(_bridge, task, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
|
||||||
|
{
|
||||||
|
RCTSendCompletionEvent(_bridge, task, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Build responses
|
||||||
|
|
||||||
|
static void RCTSendResponseEvent(RCTBridge *bridge, NSURLSessionTask *task)
|
||||||
|
{
|
||||||
|
NSURLResponse *response = task.response;
|
||||||
|
NSHTTPURLResponse *httpResponse = nil;
|
||||||
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||||
|
// Might be a local file request
|
||||||
|
httpResponse = (NSHTTPURLResponse *)response;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray *responseJSON = @[@(task.taskIdentifier),
|
||||||
|
@(httpResponse.statusCode ?: 200),
|
||||||
|
httpResponse.allHeaderFields ?: @{},
|
||||||
|
];
|
||||||
|
|
||||||
|
[bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse"
|
||||||
|
body:responseJSON];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void RCTSendDataEvent(RCTBridge *bridge, NSURLSessionDataTask *task, NSData *data)
|
||||||
|
{
|
||||||
|
// Get text encoding
|
||||||
|
NSURLResponse *response = task.response;
|
||||||
|
NSStringEncoding encoding = NSUTF8StringEncoding;
|
||||||
|
if (response.textEncodingName) {
|
||||||
|
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
|
||||||
|
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding];
|
||||||
|
if (!responseText && data.length) {
|
||||||
|
RCTLogError(@"Received data was invalid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray *responseJSON = @[@(task.taskIdentifier), responseText ?: @""];
|
||||||
|
[bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData"
|
||||||
|
body:responseJSON];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void RCTSendCompletionEvent(RCTBridge *bridge, NSURLSessionTask *task, NSError *error)
|
||||||
|
{
|
||||||
|
NSArray *responseJSON = @[@(task.taskIdentifier),
|
||||||
|
error.localizedDescription ?: [NSNull null],
|
||||||
|
];
|
||||||
|
|
||||||
|
[bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse"
|
||||||
|
body:responseJSON];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; };
|
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; };
|
||||||
|
352DA0BA1B17855800AA15A8 /* RCTHTTPQueryExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DA0B81B17855800AA15A8 /* RCTHTTPQueryExecutor.m */; };
|
||||||
58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTDataManager.m */; };
|
58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTDataManager.m */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
@ -26,6 +27,9 @@
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
1372B7351AB03E7B00659ED6 /* RCTReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTReachability.h; sourceTree = "<group>"; };
|
1372B7351AB03E7B00659ED6 /* RCTReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTReachability.h; sourceTree = "<group>"; };
|
||||||
1372B7361AB03E7B00659ED6 /* RCTReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTReachability.m; sourceTree = "<group>"; };
|
1372B7361AB03E7B00659ED6 /* RCTReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTReachability.m; sourceTree = "<group>"; };
|
||||||
|
352DA0B51B17855800AA15A8 /* RCTDataQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataQuery.h; sourceTree = "<group>"; };
|
||||||
|
352DA0B71B17855800AA15A8 /* RCTHTTPQueryExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHTTPQueryExecutor.h; sourceTree = "<group>"; };
|
||||||
|
352DA0B81B17855800AA15A8 /* RCTHTTPQueryExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTHTTPQueryExecutor.m; sourceTree = "<group>"; };
|
||||||
58B511DB1A9E6C8500147676 /* libRCTNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
58B511DB1A9E6C8500147676 /* libRCTNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
58B512061A9E6CE300147676 /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = "<group>"; };
|
58B512061A9E6CE300147676 /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = "<group>"; };
|
||||||
58B512071A9E6CE300147676 /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = "<group>"; };
|
58B512071A9E6CE300147676 /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = "<group>"; };
|
||||||
|
@ -47,6 +51,9 @@
|
||||||
children = (
|
children = (
|
||||||
58B512061A9E6CE300147676 /* RCTDataManager.h */,
|
58B512061A9E6CE300147676 /* RCTDataManager.h */,
|
||||||
58B512071A9E6CE300147676 /* RCTDataManager.m */,
|
58B512071A9E6CE300147676 /* RCTDataManager.m */,
|
||||||
|
352DA0B51B17855800AA15A8 /* RCTDataQuery.h */,
|
||||||
|
352DA0B71B17855800AA15A8 /* RCTHTTPQueryExecutor.h */,
|
||||||
|
352DA0B81B17855800AA15A8 /* RCTHTTPQueryExecutor.m */,
|
||||||
1372B7351AB03E7B00659ED6 /* RCTReachability.h */,
|
1372B7351AB03E7B00659ED6 /* RCTReachability.h */,
|
||||||
1372B7361AB03E7B00659ED6 /* RCTReachability.m */,
|
1372B7361AB03E7B00659ED6 /* RCTReachability.m */,
|
||||||
58B511DC1A9E6C8500147676 /* Products */,
|
58B511DC1A9E6C8500147676 /* Products */,
|
||||||
|
@ -121,6 +128,7 @@
|
||||||
files = (
|
files = (
|
||||||
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */,
|
1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */,
|
||||||
58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */,
|
58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */,
|
||||||
|
352DA0BA1B17855800AA15A8 /* RCTHTTPQueryExecutor.m in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -209,6 +217,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||||
"$(SRCROOT)/../../React/**",
|
"$(SRCROOT)/../../React/**",
|
||||||
|
"$(SRCROOT)/../Image/**",
|
||||||
);
|
);
|
||||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||||
OTHER_LDFLAGS = "-ObjC";
|
OTHER_LDFLAGS = "-ObjC";
|
||||||
|
@ -226,6 +235,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||||
"$(SRCROOT)/../../React/**",
|
"$(SRCROOT)/../../React/**",
|
||||||
|
"$(SRCROOT)/../Image/**",
|
||||||
);
|
);
|
||||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||||
OTHER_LDFLAGS = "-ObjC";
|
OTHER_LDFLAGS = "-ObjC";
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var FormData = require('FormData');
|
||||||
var RCTDataManager = require('NativeModules').DataManager;
|
var RCTDataManager = require('NativeModules').DataManager;
|
||||||
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
|
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
|
||||||
|
|
||||||
|
@ -82,13 +83,19 @@ class XMLHttpRequest extends XMLHttpRequestBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendImpl(method: ?string, url: ?string, headers: Object, data: any): void {
|
sendImpl(method: ?string, url: ?string, headers: Object, data: any): void {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
data = {string: data};
|
||||||
|
}
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
data = {formData: data.getParts()};
|
||||||
|
}
|
||||||
RCTDataManager.queryData(
|
RCTDataManager.queryData(
|
||||||
'http',
|
'http',
|
||||||
{
|
{
|
||||||
method: method,
|
method,
|
||||||
url: url,
|
url,
|
||||||
data: data,
|
data,
|
||||||
headers: headers,
|
headers,
|
||||||
},
|
},
|
||||||
this.onreadystatechange ? true : false,
|
this.onreadystatechange ? true : false,
|
||||||
this._didCreateRequest.bind(this)
|
this._didCreateRequest.bind(this)
|
||||||
|
|
|
@ -58,5 +58,13 @@ RCT_EXTERN BOOL RCTRunningInTestEnvironment(void);
|
||||||
// Return YES if image has an alpha component
|
// Return YES if image has an alpha component
|
||||||
RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image);
|
RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image);
|
||||||
|
|
||||||
|
// Create an NSError in the NCTErrorDomain
|
||||||
|
RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message);
|
||||||
|
|
||||||
|
// Dispatch an error + result callback on the main thread.
|
||||||
|
typedef void (^RCTResultOrErrorBlock)(NSError *error, id result);
|
||||||
|
RCT_EXTERN void RCTDispatchCallbackOnMainQueue(RCTResultOrErrorBlock callback, NSError *error, id result);
|
||||||
|
|
||||||
|
// Convert nil values to NSNull, and vice-versa
|
||||||
RCT_EXTERN id RCTNullIfNil(id value);
|
RCT_EXTERN id RCTNullIfNil(id value);
|
||||||
RCT_EXTERN id RCTNilIfNull(id value);
|
RCT_EXTERN id RCTNilIfNull(id value);
|
||||||
|
|
|
@ -282,6 +282,24 @@ BOOL RCTImageHasAlpha(CGImageRef image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSError *RCTErrorWithMessage(NSString *message)
|
||||||
|
{
|
||||||
|
NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message};
|
||||||
|
NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RCTDispatchCallbackOnMainQueue(RCTResultOrErrorBlock callback, NSError *error, id result)
|
||||||
|
{
|
||||||
|
if ([NSThread isMainThread]) {
|
||||||
|
callback(error, result);
|
||||||
|
} else {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
callback(error, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
id RCTNullIfNil(id value)
|
id RCTNullIfNil(id value)
|
||||||
{
|
{
|
||||||
return value ?: (id)kCFNull;
|
return value ?: (id)kCFNull;
|
||||||
|
|
Loading…
Reference in New Issue