first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
/**
* 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.
*
* @generated by an internal plugin build system
*/
#ifdef RN_DISABLE_OSS_PLUGIN_HEADER
// FB Internal: FBCoreModulesPlugins.h is autogenerated by the build system.
#import <React/FBCoreModulesPlugins.h>
#else
// OSS-compatibility layer
#import <Foundation/Foundation.h>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wreturn-type-c-linkage"
#ifdef __cplusplus
extern "C" {
#endif
// RCTTurboModuleManagerDelegate should call this to resolve module classes.
Class RCTCoreModulesClassProvider(const char *name);
// Lookup functions
Class RCTAccessibilityManagerCls(void) __attribute__((used));
Class RCTActionSheetManagerCls(void) __attribute__((used));
Class RCTAlertManagerCls(void) __attribute__((used));
Class RCTAppStateCls(void) __attribute__((used));
Class RCTAppearanceCls(void) __attribute__((used));
Class RCTClipboardCls(void) __attribute__((used));
Class RCTDevLoadingViewCls(void) __attribute__((used));
Class RCTDevMenuCls(void) __attribute__((used));
Class RCTDevSettingsCls(void) __attribute__((used));
Class RCTDeviceInfoCls(void) __attribute__((used));
Class RCTEventDispatcherCls(void) __attribute__((used));
Class RCTExceptionsManagerCls(void) __attribute__((used));
Class RCTI18nManagerCls(void) __attribute__((used));
Class RCTKeyboardObserverCls(void) __attribute__((used));
Class RCTLogBoxCls(void) __attribute__((used));
Class RCTPerfMonitorCls(void) __attribute__((used));
Class RCTPlatformCls(void) __attribute__((used));
Class RCTRedBoxCls(void) __attribute__((used));
Class RCTSourceCodeCls(void) __attribute__((used));
Class RCTStatusBarManagerCls(void) __attribute__((used));
Class RCTTimingCls(void) __attribute__((used));
Class RCTWebSocketModuleCls(void) __attribute__((used));
Class RCTBlobManagerCls(void) __attribute__((used));
#ifdef __cplusplus
}
#endif
#pragma GCC diagnostic pop
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@@ -0,0 +1,55 @@
/**
* 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.
*
* @generated by an internal plugin build system
*/
#ifndef RN_DISABLE_OSS_PLUGIN_HEADER
// OSS-compatibility layer
#import "CoreModulesPlugins.h"
#import <string>
#import <unordered_map>
Class RCTCoreModulesClassProvider(const char *name) {
// Intentionally leak to avoid crashing after static destructors are run.
static const auto sCoreModuleClassMap = new const std::unordered_map<std::string, Class (*)(void)>{
{"AccessibilityManager", RCTAccessibilityManagerCls},
{"ActionSheetManager", RCTActionSheetManagerCls},
{"AlertManager", RCTAlertManagerCls},
{"AppState", RCTAppStateCls},
{"Appearance", RCTAppearanceCls},
{"Clipboard", RCTClipboardCls},
{"DevLoadingView", RCTDevLoadingViewCls},
{"DevMenu", RCTDevMenuCls},
{"DevSettings", RCTDevSettingsCls},
{"DeviceInfo", RCTDeviceInfoCls},
{"EventDispatcher", RCTEventDispatcherCls},
{"ExceptionsManager", RCTExceptionsManagerCls},
{"I18nManager", RCTI18nManagerCls},
{"KeyboardObserver", RCTKeyboardObserverCls},
{"LogBox", RCTLogBoxCls},
{"PerfMonitor", RCTPerfMonitorCls},
{"PlatformConstants", RCTPlatformCls},
{"RedBox", RCTRedBoxCls},
{"SourceCode", RCTSourceCodeCls},
{"StatusBarManager", RCTStatusBarManagerCls},
{"Timing", RCTTimingCls},
{"WebSocketModule", RCTWebSocketModuleCls},
{"BlobModule", RCTBlobManagerCls},
};
auto p = sCoreModuleClassMap->find(name);
if (p != sCoreModuleClassMap->end()) {
auto classFunc = p->second;
return classFunc();
}
return nil;
}
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@@ -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.
*/
#import "RCTStatusBarManager.h"
#import "CoreModulesPlugins.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
@interface RCTStatusBarManager () <NativeStatusBarManagerIOSSpec>
@end
@implementation RCTStatusBarManager
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(getHeight : (RCTResponseSenderBlock)callback) {}
RCT_EXPORT_METHOD(setStyle : (NSString *)style animated : (BOOL)animated) {}
RCT_EXPORT_METHOD(setHidden : (BOOL)hidden withAnimation : (NSString *)withAnimation) {}
RCT_EXPORT_METHOD(setNetworkActivityIndicatorVisible : (BOOL)visible) {}
- (facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants>)getConstants
{
return facebook::react::typedConstants<JS::NativeStatusBarManagerIOS::Constants>({
.HEIGHT = 0,
.DEFAULT_BACKGROUND_COLOR = std::nullopt,
});
}
- (facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants>)constantsToExport
{
return (facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants>)[self getConstants];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeStatusBarManagerIOSSpecJSI>(params);
}
@end
Class RCTStatusBarManagerCls(void)
{
return RCTStatusBarManager.class;
}

View File

@@ -0,0 +1,24 @@
/*
* 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.
*/
#import "RCTAccessibilityManager.h"
#import <React/RCTDefines.h>
NS_ASSUME_NONNULL_BEGIN
RCT_EXTERN_C_BEGIN
// Only to be used for testing and internal tooling. Do not use this in
// production.
void RCTAccessibilityManagerSetIsVoiceOverEnabled(
RCTAccessibilityManager *accessibilityManager,
BOOL isVoiceOverEnabled);
RCT_EXTERN_C_END
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,44 @@
/*
* 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.
*/
#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTBridgeProxy.h>
extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; // posted when multiplier is changed
@interface RCTAccessibilityManager : NSObject <RCTBridgeModule>
@property (nonatomic, readonly) CGFloat multiplier;
/// map from UIKit categories to multipliers
@property (nonatomic, copy) NSDictionary<NSString *, NSNumber *> *multipliers;
@property (nonatomic, assign) BOOL isBoldTextEnabled;
@property (nonatomic, assign) BOOL isGrayscaleEnabled;
@property (nonatomic, assign) BOOL isInvertColorsEnabled;
@property (nonatomic, assign) BOOL isReduceMotionEnabled;
@property (nonatomic, assign) BOOL isDarkerSystemColorsEnabled;
@property (nonatomic, assign) BOOL prefersCrossFadeTransitions;
@property (nonatomic, assign) BOOL isReduceTransparencyEnabled;
@property (nonatomic, assign) BOOL isVoiceOverEnabled;
@end
@interface RCTBridge (RCTAccessibilityManager)
@property (nonatomic, readonly) RCTAccessibilityManager *accessibilityManager;
@end
@interface RCTBridgeProxy (RCTAccessibilityManager)
@property (nonatomic, readonly) RCTAccessibilityManager *accessibilityManager;
@end

View File

@@ -0,0 +1,442 @@
/*
* 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.
*/
#import "RCTAccessibilityManager.h"
#import "RCTAccessibilityManager+Internal.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTLog.h>
#import <React/RCTUIManager.h>
#import "CoreModulesPlugins.h"
NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification =
@"RCTAccessibilityManagerDidUpdateMultiplierNotification";
@interface RCTAccessibilityManager () <NativeAccessibilityManagerSpec>
@property (nonatomic, copy) NSString *contentSizeCategory;
@property (nonatomic, assign) CGFloat multiplier;
@end
@implementation RCTAccessibilityManager
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
@synthesize moduleRegistry = _moduleRegistry;
@synthesize multipliers = _multipliers;
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (instancetype)init
{
if (self = [super init]) {
_multiplier = 1.0;
// TODO: can this be moved out of the startup path?
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveNewContentSizeCategory:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(accessibilityAnnouncementDidFinish:)
name:UIAccessibilityAnnouncementDidFinishNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(boldTextStatusDidChange:)
name:UIAccessibilityBoldTextStatusDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(grayscaleStatusDidChange:)
name:UIAccessibilityGrayscaleStatusDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(invertColorsStatusDidChange:)
name:UIAccessibilityInvertColorsStatusDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reduceMotionStatusDidChange:)
name:UIAccessibilityReduceMotionStatusDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(darkerSystemColorsDidChange:)
name:UIAccessibilityDarkerSystemColorsStatusDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reduceTransparencyStatusDidChange:)
name:UIAccessibilityReduceTransparencyStatusDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(voiceVoiceOverStatusDidChange:)
name:UIAccessibilityVoiceOverStatusDidChangeNotification
object:nil];
self.contentSizeCategory = RCTSharedApplication().preferredContentSizeCategory;
_isBoldTextEnabled = UIAccessibilityIsBoldTextEnabled();
_isGrayscaleEnabled = UIAccessibilityIsGrayscaleEnabled();
_isInvertColorsEnabled = UIAccessibilityIsInvertColorsEnabled();
_isReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
_isDarkerSystemColorsEnabled = UIAccessibilityDarkerSystemColorsEnabled();
_isReduceTransparencyEnabled = UIAccessibilityIsReduceTransparencyEnabled();
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
}
return self;
}
- (void)didReceiveNewContentSizeCategory:(NSNotification *)note
{
self.contentSizeCategory = note.userInfo[UIContentSizeCategoryNewValueKey];
}
- (void)accessibilityAnnouncementDidFinish:(__unused NSNotification *)notification
{
NSDictionary *userInfo = notification.userInfo;
// Response dictionary to populate the event with.
NSDictionary *response = @{
@"announcement" : userInfo[UIAccessibilityAnnouncementKeyStringValue],
@"success" : userInfo[UIAccessibilityAnnouncementKeyWasSuccessful]
};
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"announcementFinished" body:response];
#pragma clang diagnostic pop
}
- (void)boldTextStatusDidChange:(__unused NSNotification *)notification
{
BOOL newBoldTextEnabled = UIAccessibilityIsBoldTextEnabled();
if (_isBoldTextEnabled != newBoldTextEnabled) {
_isBoldTextEnabled = newBoldTextEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"boldTextChanged"
body:@(_isBoldTextEnabled)];
#pragma clang diagnostic pop
}
}
- (void)grayscaleStatusDidChange:(__unused NSNotification *)notification
{
BOOL newGrayscaleEnabled = UIAccessibilityIsGrayscaleEnabled();
if (_isGrayscaleEnabled != newGrayscaleEnabled) {
_isGrayscaleEnabled = newGrayscaleEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"grayscaleChanged"
body:@(_isGrayscaleEnabled)];
#pragma clang diagnostic pop
}
}
- (void)invertColorsStatusDidChange:(__unused NSNotification *)notification
{
BOOL newInvertColorsEnabled = UIAccessibilityIsInvertColorsEnabled();
if (_isInvertColorsEnabled != newInvertColorsEnabled) {
_isInvertColorsEnabled = newInvertColorsEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"invertColorsChanged"
body:@(_isInvertColorsEnabled)];
#pragma clang diagnostic pop
}
}
- (void)reduceMotionStatusDidChange:(__unused NSNotification *)notification
{
BOOL newReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
if (_isReduceMotionEnabled != newReduceMotionEnabled) {
_isReduceMotionEnabled = newReduceMotionEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"reduceMotionChanged"
body:@(_isReduceMotionEnabled)];
#pragma clang diagnostic pop
}
}
- (void)darkerSystemColorsDidChange:(__unused NSNotification *)notification
{
BOOL newDarkerSystemColorsEnabled = UIAccessibilityDarkerSystemColorsEnabled();
if (_isDarkerSystemColorsEnabled != newDarkerSystemColorsEnabled) {
_isDarkerSystemColorsEnabled = newDarkerSystemColorsEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"darkerSystemColorsChanged"
body:@(_isDarkerSystemColorsEnabled)];
#pragma clang diagnostic pop
}
}
- (void)reduceTransparencyStatusDidChange:(__unused NSNotification *)notification
{
BOOL newReduceTransparencyEnabled = UIAccessibilityIsReduceTransparencyEnabled();
if (_isReduceTransparencyEnabled != newReduceTransparencyEnabled) {
_isReduceTransparencyEnabled = newReduceTransparencyEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"reduceTransparencyChanged"
body:@(_isReduceTransparencyEnabled)];
#pragma clang diagnostic pop
}
}
- (void)voiceVoiceOverStatusDidChange:(__unused NSNotification *)notification
{
BOOL isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
[self _setIsVoiceOverEnabled:isVoiceOverEnabled];
}
- (void)_setIsVoiceOverEnabled:(BOOL)isVoiceOverEnabled
{
if (_isVoiceOverEnabled != isVoiceOverEnabled) {
_isVoiceOverEnabled = isVoiceOverEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"screenReaderChanged"
body:@(_isVoiceOverEnabled)];
#pragma clang diagnostic pop
}
}
- (void)setContentSizeCategory:(NSString *)contentSizeCategory
{
if (_contentSizeCategory != contentSizeCategory) {
_contentSizeCategory = [contentSizeCategory copy];
[self invalidateMultiplier];
}
}
- (void)invalidateMultiplier
{
self.multiplier = [self multiplierForContentSizeCategory:_contentSizeCategory];
[[NSNotificationCenter defaultCenter] postNotificationName:RCTAccessibilityManagerDidUpdateMultiplierNotification
object:self];
}
- (CGFloat)multiplierForContentSizeCategory:(NSString *)category
{
NSNumber *m = self.multipliers[category];
if (m.doubleValue <= 0.0) {
RCTLogError(@"Can't determine multiplier for category %@. Using 1.0.", category);
m = @1.0;
}
return m.doubleValue;
}
- (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
{
if (_multipliers != multipliers) {
_multipliers = [multipliers copy];
[self invalidateMultiplier];
}
}
- (NSDictionary<NSString *, NSNumber *> *)multipliers
{
if (_multipliers == nil) {
_multipliers = @{
UIContentSizeCategoryExtraSmall : @0.823,
UIContentSizeCategorySmall : @0.882,
UIContentSizeCategoryMedium : @0.941,
UIContentSizeCategoryLarge : @1.0,
UIContentSizeCategoryExtraLarge : @1.118,
UIContentSizeCategoryExtraExtraLarge : @1.235,
UIContentSizeCategoryExtraExtraExtraLarge : @1.353,
UIContentSizeCategoryAccessibilityMedium : @1.786,
UIContentSizeCategoryAccessibilityLarge : @2.143,
UIContentSizeCategoryAccessibilityExtraLarge : @2.643,
UIContentSizeCategoryAccessibilityExtraExtraLarge : @3.143,
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge : @3.571
};
}
return _multipliers;
}
RCT_EXPORT_METHOD(
setAccessibilityContentSizeMultipliers : (
JS::NativeAccessibilityManager::SpecSetAccessibilityContentSizeMultipliersJSMultipliers &)JSMultipliers)
{
NSMutableDictionary<NSString *, NSNumber *> *multipliers = [NSMutableDictionary new];
setMultipliers(multipliers, UIContentSizeCategoryExtraSmall, JSMultipliers.extraSmall());
setMultipliers(multipliers, UIContentSizeCategorySmall, JSMultipliers.small());
setMultipliers(multipliers, UIContentSizeCategoryMedium, JSMultipliers.medium());
setMultipliers(multipliers, UIContentSizeCategoryLarge, JSMultipliers.large());
setMultipliers(multipliers, UIContentSizeCategoryExtraLarge, JSMultipliers.extraLarge());
setMultipliers(multipliers, UIContentSizeCategoryExtraExtraLarge, JSMultipliers.extraExtraLarge());
setMultipliers(multipliers, UIContentSizeCategoryExtraExtraExtraLarge, JSMultipliers.extraExtraExtraLarge());
setMultipliers(multipliers, UIContentSizeCategoryAccessibilityMedium, JSMultipliers.accessibilityMedium());
setMultipliers(multipliers, UIContentSizeCategoryAccessibilityLarge, JSMultipliers.accessibilityLarge());
setMultipliers(multipliers, UIContentSizeCategoryAccessibilityExtraLarge, JSMultipliers.accessibilityExtraLarge());
setMultipliers(
multipliers, UIContentSizeCategoryAccessibilityExtraExtraLarge, JSMultipliers.accessibilityExtraExtraLarge());
setMultipliers(
multipliers,
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge,
JSMultipliers.accessibilityExtraExtraExtraLarge());
self.multipliers = multipliers;
}
static void setMultipliers(
NSMutableDictionary<NSString *, NSNumber *> *multipliers,
NSString *key,
std::optional<double> optionalDouble)
{
if (optionalDouble.has_value()) {
multipliers[key] = @(optionalDouble.value());
}
}
RCT_EXPORT_METHOD(setAccessibilityFocus : (double)reactTag)
{
dispatch_async(dispatch_get_main_queue(), ^{
UIView *view = [self.viewRegistry_DEPRECATED viewForReactTag:@(reactTag)];
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, view);
});
}
RCT_EXPORT_METHOD(announceForAccessibility : (NSString *)announcement)
{
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}
RCT_EXPORT_METHOD(
announceForAccessibilityWithOptions : (NSString *)announcement options : (
JS::NativeAccessibilityManager::SpecAnnounceForAccessibilityWithOptionsOptions &)options)
{
NSMutableDictionary<NSString *, NSNumber *> *attrsDictionary = [NSMutableDictionary new];
if (options.queue()) {
attrsDictionary[UIAccessibilitySpeechAttributeQueueAnnouncement] = @(*(options.queue()) ? YES : NO);
}
if (attrsDictionary.count > 0) {
NSAttributedString *announcementWithAttrs = [[NSAttributedString alloc] initWithString:announcement
attributes:attrsDictionary];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementWithAttrs);
} else {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}
}
RCT_EXPORT_METHOD(getMultiplier : (RCTResponseSenderBlock)callback)
{
if (callback) {
callback(@[ @(self.multiplier) ]);
}
}
RCT_EXPORT_METHOD(
getCurrentBoldTextState : (RCTResponseSenderBlock)onSuccess onError : (__unused RCTResponseSenderBlock)onError)
{
onSuccess(@[ @(_isBoldTextEnabled) ]);
}
RCT_EXPORT_METHOD(
getCurrentGrayscaleState : (RCTResponseSenderBlock)onSuccess onError : (__unused RCTResponseSenderBlock)onError)
{
onSuccess(@[ @(_isGrayscaleEnabled) ]);
}
RCT_EXPORT_METHOD(
getCurrentInvertColorsState : (RCTResponseSenderBlock)onSuccess onError : (__unused RCTResponseSenderBlock)onError)
{
onSuccess(@[ @(_isInvertColorsEnabled) ]);
}
RCT_EXPORT_METHOD(
getCurrentReduceMotionState : (RCTResponseSenderBlock)onSuccess onError : (__unused RCTResponseSenderBlock)onError)
{
onSuccess(@[ @(_isReduceMotionEnabled) ]);
}
RCT_EXPORT_METHOD(
getCurrentDarkerSystemColorsState : (RCTResponseSenderBlock)onSuccess onError : (__unused RCTResponseSenderBlock)
onError)
{
onSuccess(@[ @(_isDarkerSystemColorsEnabled) ]);
}
RCT_EXPORT_METHOD(
getCurrentPrefersCrossFadeTransitionsState : (RCTResponseSenderBlock)
onSuccess onError : (__unused RCTResponseSenderBlock)onError)
{
if (@available(iOS 14.0, *)) {
onSuccess(@[ @(UIAccessibilityPrefersCrossFadeTransitions()) ]);
} else {
onSuccess(@[ @(false) ]);
}
}
RCT_EXPORT_METHOD(
getCurrentReduceTransparencyState : (RCTResponseSenderBlock)onSuccess onError : (__unused RCTResponseSenderBlock)
onError)
{
onSuccess(@[ @(_isReduceTransparencyEnabled) ]);
}
RCT_EXPORT_METHOD(
getCurrentVoiceOverState : (RCTResponseSenderBlock)onSuccess onError : (__unused RCTResponseSenderBlock)onError)
{
onSuccess(@[ @(_isVoiceOverEnabled) ]);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeAccessibilityManagerSpecJSI>(params);
}
#pragma mark - Internal
void RCTAccessibilityManagerSetIsVoiceOverEnabled(
RCTAccessibilityManager *accessibilityManager,
BOOL isVoiceOverEnabled)
{
[accessibilityManager _setIsVoiceOverEnabled:isVoiceOverEnabled];
}
@end
@implementation RCTBridge (RCTAccessibilityManager)
- (RCTAccessibilityManager *)accessibilityManager
{
return [self moduleForClass:[RCTAccessibilityManager class]];
}
@end
@implementation RCTBridgeProxy (RCTAccessibilityManager)
- (RCTAccessibilityManager *)accessibilityManager
{
return [self moduleForClass:[RCTAccessibilityManager class]];
}
@end
Class RCTAccessibilityManagerCls(void)
{
return RCTAccessibilityManager.class;
}

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
@interface RCTActionSheetManager : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,319 @@
/*
* 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.
*/
#import <React/RCTActionSheetManager.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTLog.h>
#import <React/RCTUIManager.h>
#import <React/RCTUtils.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <RCTTypeSafety/RCTConvertHelpers.h>
#import "CoreModulesPlugins.h"
using namespace facebook::react;
@interface RCTActionSheetManager () <NativeActionSheetManagerSpec>
@property (nonatomic, strong) NSMutableArray<UIAlertController *> *alertControllers;
@end
@implementation RCTActionSheetManager
- (instancetype)init
{
self = [super init];
if (self != nullptr) {
_alertControllers = [NSMutableArray new];
}
return self;
}
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
RCT_EXPORT_MODULE()
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
- (void)presentViewController:(UIViewController *)alertController
onParentViewController:(UIViewController *)parentViewController
anchorViewTag:(NSNumber *)anchorViewTag
{
alertController.modalPresentationStyle = UIModalPresentationPopover;
UIView *sourceView = parentViewController.view;
if (anchorViewTag != nullptr) {
sourceView = [self.viewRegistry_DEPRECATED viewForReactTag:anchorViewTag];
} else {
alertController.popoverPresentationController.permittedArrowDirections = 0;
}
if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
// These information only make sense for iPad, where the action sheet needs
// to be presented from a specific anchor point.
// Before iOS 26, if these information were passed on an iOS app, the action sheet
// was ignoring them. after iOS 26, they are took into consideration and the
// sheet behavior changes, for example the user can interact with the background
// By applying these informations only to the iPad use case, we revert to the previous
// behavior.
alertController.popoverPresentationController.sourceView = sourceView;
alertController.popoverPresentationController.sourceRect = sourceView.bounds;
}
[parentViewController presentViewController:alertController animated:YES completion:nil];
}
RCT_EXPORT_METHOD(
showActionSheetWithOptions : (JS::NativeActionSheetManager::SpecShowActionSheetWithOptionsOptions &)
options callback : (RCTResponseSenderBlock)callback)
{
if (RCTRunningInAppExtension()) {
RCTLogError(@"Unable to show action sheet from app extension");
return;
}
NSString *title = options.title();
NSString *message = options.message();
NSArray<NSString *> *buttons = RCTConvertOptionalVecToArray(options.options(), ^id(NSString *element) {
return element;
});
NSArray<NSNumber *> *disabledButtonIndices;
NSInteger cancelButtonIndex =
options.cancelButtonIndex() ? [RCTConvert NSInteger:@(*options.cancelButtonIndex())] : -1;
NSArray<NSNumber *> *destructiveButtonIndices;
if (options.disabledButtonIndices()) {
disabledButtonIndices = RCTConvertVecToArray(*options.disabledButtonIndices(), ^id(double element) {
return @(element);
});
}
if (options.destructiveButtonIndices()) {
destructiveButtonIndices = RCTConvertVecToArray(*options.destructiveButtonIndices(), ^id(double element) {
return @(element);
});
} else {
NSNumber *destructiveButtonIndex = @-1;
destructiveButtonIndices = @[ destructiveButtonIndex ];
}
NSNumber *anchor = [RCTConvert NSNumber:options.anchor() ? @(*options.anchor()) : nil];
UIColor *tintColor = [RCTConvert UIColor:options.tintColor() ? @(*options.tintColor()) : nil];
UIColor *cancelButtonTintColor =
[RCTConvert UIColor:options.cancelButtonTintColor() ? @(*options.cancelButtonTintColor()) : nil];
UIColor *disabledButtonTintColor =
[RCTConvert UIColor:options.disabledButtonTintColor() ? @(*options.disabledButtonTintColor()) : nil];
NSString *userInterfaceStyle = [RCTConvert NSString:options.userInterfaceStyle()];
dispatch_async(dispatch_get_main_queue(), ^{
UIViewController *controller = RCTPresentedViewController();
if (controller == nil) {
RCTLogError(
@"Tried to display action sheet but there is no application window. options: %@", @{
@"title" : title ?: @"(null)",
@"message" : message ?: @"(null)",
@"options" : buttons,
@"cancelButtonIndex" : @(cancelButtonIndex),
@"destructiveButtonIndices" : destructiveButtonIndices,
@"anchor" : anchor ?: @"(null)",
@"tintColor" : tintColor ?: @"(null)",
@"cancelButtonTintColor" : cancelButtonTintColor ?: @"(null)",
@"disabledButtonTintColor" : disabledButtonTintColor ?: @"(null)",
@"disabledButtonIndices" : disabledButtonIndices ?: @"(null)",
});
return;
}
/*
* The `anchor` option takes a view to set as the anchor for the share
* popup to point to, on iPads running iOS 8. If it is not passed, it
* defaults to centering the share popup on screen without any arrows.
*/
NSNumber *anchorViewTag = anchor;
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleActionSheet];
NSInteger index = 0;
bool isCancelButtonIndex = false;
// The handler for a button might get called more than once when tapping outside
// the action sheet on iPad. RCTResponseSenderBlock can only be called once so
// keep track of callback invocation here.
__block bool callbackInvoked = false;
for (NSString *option in buttons) {
UIAlertActionStyle style = UIAlertActionStyleDefault;
if ([destructiveButtonIndices containsObject:@(index)]) {
style = UIAlertActionStyleDestructive;
} else if (index == cancelButtonIndex) {
style = UIAlertActionStyleCancel;
isCancelButtonIndex = true;
}
NSInteger localIndex = index;
UIAlertAction *actionButton = [UIAlertAction actionWithTitle:option
style:style
handler:^(__unused UIAlertAction *action) {
if (!callbackInvoked) {
callbackInvoked = true;
[self->_alertControllers removeObject:alertController];
callback(@[ @(localIndex) ]);
}
}];
if (isCancelButtonIndex && cancelButtonTintColor != NULL) {
[actionButton setValue:cancelButtonTintColor forKey:@"titleTextColor"];
} else if (style != UIAlertActionStyleDestructive) {
// iOS 26 does not apply the tint color automatically to all the buttons.
// So we ca forcibly apply the tint color to all the buttons that are not
// destructive (which usually have a different tint color) nor they are
// cancel buttons (which might have a different tint color).
// This makes the Action Sheet behave as in iOS < 26.
[actionButton setValue:tintColor forKey:@"titleTextColor"];
}
[alertController addAction:actionButton];
index++;
}
if (disabledButtonIndices != nullptr) {
for (NSNumber *disabledButtonIndex in disabledButtonIndices) {
if ([disabledButtonIndex integerValue] < buttons.count) {
UIAlertAction *action = alertController.actions[[disabledButtonIndex integerValue]];
[action setEnabled:false];
if (disabledButtonTintColor != nullptr) {
[action setValue:disabledButtonTintColor forKey:@"titleTextColor"];
}
} else {
RCTLogError(
@"Index %@ from `disabledButtonIndices` is out of bounds. Maximum index value is %@.",
@([disabledButtonIndex integerValue]),
@(buttons.count - 1));
return;
}
}
}
alertController.view.tintColor = tintColor;
if (userInterfaceStyle == nil || [userInterfaceStyle isEqualToString:@""]) {
alertController.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
} else if ([userInterfaceStyle isEqualToString:@"dark"]) {
alertController.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
} else if ([userInterfaceStyle isEqualToString:@"light"]) {
alertController.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
[self->_alertControllers addObject:alertController];
[self presentViewController:alertController onParentViewController:controller anchorViewTag:anchorViewTag];
});
}
RCT_EXPORT_METHOD(dismissActionSheet)
{
if (_alertControllers.count == 0) {
RCTLogWarn(@"Unable to dismiss action sheet");
}
UIAlertController *alertController = [_alertControllers lastObject];
dispatch_async(dispatch_get_main_queue(), ^{
[alertController dismissViewControllerAnimated:YES completion:nil];
[self->_alertControllers removeLastObject];
});
}
RCT_EXPORT_METHOD(
showShareActionSheetWithOptions : (JS::NativeActionSheetManager::SpecShowShareActionSheetWithOptionsOptions &)
options failureCallback : (RCTResponseSenderBlock)failureCallback successCallback : (RCTResponseSenderBlock)
successCallback)
{
if (RCTRunningInAppExtension()) {
RCTLogError(@"Unable to show action sheet from app extension");
return;
}
NSMutableArray<id> *items = [NSMutableArray array];
NSString *message = options.message();
NSURL *URL = [RCTConvert NSURL:options.url()];
NSString *subject = options.subject();
NSArray *excludedActivityTypes =
RCTConvertOptionalVecToArray(options.excludedActivityTypes(), ^id(NSString *element) {
return element;
});
NSString *userInterfaceStyle = [RCTConvert NSString:options.userInterfaceStyle()];
NSNumber *anchorViewTag = [RCTConvert NSNumber:options.anchor() ? @(*options.anchor()) : nil];
UIColor *tintColor = [RCTConvert UIColor:options.tintColor() ? @(*options.tintColor()) : nil];
dispatch_async(dispatch_get_main_queue(), ^{
if (message != nullptr) {
[items addObject:message];
}
if (URL != nullptr) {
if ([URL.scheme.lowercaseString isEqualToString:@"data"]) {
NSError *error;
NSData *data = [NSData dataWithContentsOfURL:URL options:(NSDataReadingOptions)0 error:&error];
if (data == nullptr) {
failureCallback(@[ RCTJSErrorFromNSError(error) ]);
return;
}
[items addObject:data];
} else {
[items addObject:URL];
}
}
if (items.count == 0) {
RCTLogError(@"No `url` or `message` to share");
return;
}
UIActivityViewController *shareController = [[UIActivityViewController alloc] initWithActivityItems:items
applicationActivities:nil];
if (subject != nullptr) {
[shareController setValue:subject forKey:@"subject"];
}
if (excludedActivityTypes != nullptr) {
shareController.excludedActivityTypes = excludedActivityTypes;
}
UIViewController *controller = RCTPresentedViewController();
shareController.completionWithItemsHandler =
^(NSString *activityType, BOOL completed, __unused NSArray *returnedItems, NSError *activityError) {
if (activityError != nullptr) {
failureCallback(@[ RCTJSErrorFromNSError(activityError) ]);
} else if (completed || activityType == nil) {
successCallback(@[ @(completed), RCTNullIfNil(activityType) ]);
}
};
shareController.view.tintColor = tintColor;
if (userInterfaceStyle == nil || [userInterfaceStyle isEqualToString:@""]) {
shareController.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
} else if ([userInterfaceStyle isEqualToString:@"dark"]) {
shareController.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
} else if ([userInterfaceStyle isEqualToString:@"light"]) {
shareController.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
[self presentViewController:shareController onParentViewController:controller anchorViewTag:anchorViewTag];
});
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeActionSheetManagerSpecJSI>(params);
}
@end
Class RCTActionSheetManagerCls(void)
{
return RCTActionSheetManager.class;
}

View File

@@ -0,0 +1,15 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
@interface RCTAlertController : UIAlertController
- (void)show:(BOOL)animated completion:(void (^)(void))completion;
- (void)hide;
@end

View File

@@ -0,0 +1,62 @@
/*
* 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.
*/
#import <React/RCTUtils.h>
#import <React/RCTAlertController.h>
@interface RCTAlertController ()
@property (nonatomic, strong) UIWindow *alertWindow;
@end
@implementation RCTAlertController
- (UIWindow *)alertWindow
{
if (_alertWindow == nil) {
UIWindowScene *scene = RCTKeyWindow().windowScene;
if (scene != nil) {
_alertWindow = [[UIWindow alloc] initWithWindowScene:scene];
} else {
_alertWindow = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
}
if (_alertWindow != nullptr) {
_alertWindow.rootViewController = [UIViewController new];
_alertWindow.windowLevel = UIWindowLevelAlert + 1;
}
}
return _alertWindow;
}
- (void)show:(BOOL)animated completion:(void (^)(void))completion
{
UIUserInterfaceStyle style = self.overrideUserInterfaceStyle;
if (style == UIUserInterfaceStyleUnspecified) {
UIUserInterfaceStyle overriddenStyle = RCTKeyWindow().overrideUserInterfaceStyle;
style = (overriddenStyle != 0) ? overriddenStyle : UIUserInterfaceStyleUnspecified;
}
self.overrideUserInterfaceStyle = style;
[self.alertWindow makeKeyAndVisible];
[self.alertWindow.rootViewController presentViewController:self animated:animated completion:completion];
}
- (void)hide
{
[_alertWindow setHidden:YES];
_alertWindow.windowScene = nil;
_alertWindow = nil;
}
@end

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTInvalidating.h>
typedef NS_ENUM(NSInteger, RCTAlertViewStyle) {
RCTAlertViewStyleDefault = 0,
RCTAlertViewStyleSecureTextInput,
RCTAlertViewStylePlainTextInput,
RCTAlertViewStyleLoginAndPasswordInput
};
@interface RCTAlertManager : NSObject <RCTBridgeModule, RCTInvalidating>
@end

View File

@@ -0,0 +1,215 @@
/*
* 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.
*/
#import "RCTAlertManager.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <RCTTypeSafety/RCTConvertHelpers.h>
#import <React/RCTAssert.h>
#import <React/RCTConvert.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
#import "RCTAlertController.h"
@implementation RCTConvert (UIAlertViewStyle)
RCT_ENUM_CONVERTER(
RCTAlertViewStyle,
(@{
@"default" : @(RCTAlertViewStyleDefault),
@"secure-text" : @(RCTAlertViewStyleSecureTextInput),
@"plain-text" : @(RCTAlertViewStylePlainTextInput),
@"login-password" : @(RCTAlertViewStyleLoginAndPasswordInput),
}),
RCTAlertViewStyleDefault,
integerValue)
@end
@interface RCTAlertManager () <NativeAlertManagerSpec>
@end
@implementation RCTAlertManager {
NSHashTable *_alertControllers;
}
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
RCTExecuteOnMainQueue(^{
for (UIAlertController *alertController in self->_alertControllers) {
[alertController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
});
}
/**
* @param {NSDictionary} args Dictionary of the form
*
* @{
* @"message": @"<Alert message>",
* @"buttons": @[
* @{@"<key1>": @"<title1>"},
* @{@"<key2>": @"<title2>"},
* ],
* @"cancelButtonKey": @"<key2>",
* }
* The key from the `buttons` dictionary is passed back in the callback on click.
* Buttons are displayed in the order they are specified.
*/
RCT_EXPORT_METHOD(alertWithArgs : (JS::NativeAlertManager::Args &)args callback : (RCTResponseSenderBlock)callback)
{
NSString *title = [RCTConvert NSString:args.title()];
NSString *message = [RCTConvert NSString:args.message()];
RCTAlertViewStyle type = [RCTConvert RCTAlertViewStyle:args.type()];
NSArray<NSDictionary *> *buttons =
[RCTConvert NSDictionaryArray:RCTConvertOptionalVecToArray(args.buttons(), ^id(id<NSObject> element) {
return element;
})];
NSString *defaultValue = [RCTConvert NSString:args.defaultValue()];
NSString *cancelButtonKey = [RCTConvert NSString:args.cancelButtonKey()];
NSString *destructiveButtonKey = [RCTConvert NSString:args.destructiveButtonKey()];
NSString *preferredButtonKey = [RCTConvert NSString:args.preferredButtonKey()];
UIKeyboardType keyboardType = [RCTConvert UIKeyboardType:args.keyboardType()];
UIUserInterfaceStyle userInterfaceStyle = [RCTConvert UIUserInterfaceStyle:args.userInterfaceStyle()];
if ((title == nullptr) && (message == nullptr)) {
RCTLogError(@"Must specify either an alert title, or message, or both");
return;
}
if (buttons.count == 0) {
if (type == RCTAlertViewStyleDefault) {
buttons = @[ @{@"0" : RCTUIKitLocalizedString(@"OK")} ];
cancelButtonKey = @"0";
} else {
buttons = @[
@{@"0" : RCTUIKitLocalizedString(@"OK")},
@{@"1" : RCTUIKitLocalizedString(@"Cancel")},
];
cancelButtonKey = @"1";
}
}
RCTExecuteOnMainQueue(^{
RCTAlertController *alertController = [RCTAlertController alertControllerWithTitle:title
message:nil
preferredStyle:UIAlertControllerStyleAlert];
alertController.overrideUserInterfaceStyle = userInterfaceStyle;
switch (type) {
case RCTAlertViewStylePlainTextInput: {
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = NO;
textField.text = defaultValue;
textField.keyboardType = keyboardType;
}];
break;
}
case RCTAlertViewStyleSecureTextInput: {
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = RCTUIKitLocalizedString(@"Password");
textField.secureTextEntry = YES;
textField.text = defaultValue;
textField.keyboardType = keyboardType;
}];
break;
}
case RCTAlertViewStyleLoginAndPasswordInput: {
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = RCTUIKitLocalizedString(@"Login");
textField.text = defaultValue;
textField.keyboardType = keyboardType;
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = RCTUIKitLocalizedString(@"Password");
textField.secureTextEntry = YES;
}];
break;
}
case RCTAlertViewStyleDefault:
break;
}
alertController.message = message;
for (NSDictionary<NSString *, id> *button in buttons) {
if (button.count != 1) {
RCTLogError(@"Button definitions should have exactly one key.");
}
NSString *buttonKey = button.allKeys.firstObject;
NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]];
UIAlertActionStyle buttonStyle = UIAlertActionStyleDefault;
if ([buttonKey isEqualToString:cancelButtonKey]) {
buttonStyle = UIAlertActionStyleCancel;
} else if ([buttonKey isEqualToString:destructiveButtonKey]) {
buttonStyle = UIAlertActionStyleDestructive;
}
__weak RCTAlertController *weakAlertController = alertController;
UIAlertAction *alertAction =
[UIAlertAction actionWithTitle:buttonTitle
style:buttonStyle
handler:^(__unused UIAlertAction *action) {
switch (type) {
case RCTAlertViewStylePlainTextInput:
case RCTAlertViewStyleSecureTextInput:
callback(@[ buttonKey, [weakAlertController.textFields.firstObject text] ]);
[weakAlertController hide];
break;
case RCTAlertViewStyleLoginAndPasswordInput: {
NSDictionary<NSString *, NSString *> *loginCredentials = @{
@"login" : [weakAlertController.textFields.firstObject text],
@"password" : [weakAlertController.textFields.lastObject text]
};
callback(@[ buttonKey, loginCredentials ]);
[weakAlertController hide];
break;
}
case RCTAlertViewStyleDefault:
callback(@[ buttonKey ]);
[weakAlertController hide];
break;
}
}];
[alertController addAction:alertAction];
if ([buttonKey isEqualToString:preferredButtonKey]) {
[alertController setPreferredAction:alertAction];
}
}
if (self->_alertControllers == nullptr) {
self->_alertControllers = [NSHashTable weakObjectsHashTable];
}
[self->_alertControllers addObject:alertController];
[alertController show:YES completion:nil];
});
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeAlertManagerSpecJSI>(params);
}
@end
Class RCTAlertManagerCls(void)
{
return RCTAlertManager.class;
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
#import <React/RCTEventEmitter.h>
@interface RCTAppState : RCTEventEmitter
@end

View File

@@ -0,0 +1,154 @@
/*
* 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.
*/
#import "RCTAppState.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTAssert.h>
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTInitializing.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
static NSString *RCTCurrentAppState()
{
static NSDictionary *states;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
states = @{
@(UIApplicationStateActive) : @"active",
@(UIApplicationStateBackground) : @"background",
@(UIApplicationStateInactive) : @"inactive"
};
});
if (RCTRunningInAppExtension()) {
return @"extension";
}
return states[@(RCTSharedApplication().applicationState)] ?: @"unknown";
}
@interface RCTAppState () <NativeAppStateSpec, RCTInitializing>
@end
@implementation RCTAppState {
NSString *_lastKnownState;
facebook::react::ModuleConstants<JS::NativeAppState::Constants> _constants;
}
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)initialize
{
_constants = facebook::react::typedConstants<JS::NativeAppState::Constants>({
.initialAppState = RCTCurrentAppState(),
});
}
- (facebook::react::ModuleConstants<JS::NativeAppState::Constants>)constantsToExport
{
return (facebook::react::ModuleConstants<JS::NativeAppState::Constants>)[self getConstants];
}
- (facebook::react::ModuleConstants<JS::NativeAppState::Constants>)getConstants
{
return _constants;
}
#pragma mark - Lifecycle
- (NSArray<NSString *> *)supportedEvents
{
return @[ @"appStateDidChange", @"memoryWarning" ];
}
- (void)startObserving
{
for (NSString *name in @[
UIApplicationDidBecomeActiveNotification,
UIApplicationDidEnterBackgroundNotification,
UIApplicationDidFinishLaunchingNotification,
UIApplicationWillResignActiveNotification,
UIApplicationWillEnterForegroundNotification
]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleAppStateDidChange:)
name:name
object:nil];
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleMemoryWarning)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
- (void)stopObserving
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - App Notification Methods
- (void)handleMemoryWarning
{
[self sendEventWithName:@"memoryWarning" body:nil];
}
- (void)handleAppStateDidChange:(NSNotification *)notification
{
NSString *newState;
if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) {
newState = @"inactive";
} else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) {
newState = @"background";
} else {
newState = RCTCurrentAppState();
}
if (![newState isEqualToString:_lastKnownState]) {
_lastKnownState = newState;
[self sendEventWithName:@"appStateDidChange" body:@{@"app_state" : _lastKnownState}];
}
}
#pragma mark - Public API
/**
* Get the current background/foreground state of the app
*/
RCT_EXPORT_METHOD(getCurrentAppState : (RCTResponseSenderBlock)callback error : (__unused RCTResponseSenderBlock)error)
{
callback(@[ @{@"app_state" : RCTCurrentAppState()} ]);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeAppStateSpecJSI>(params);
}
@end
Class RCTAppStateCls(void)
{
return RCTAppState.class;
}

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTConvert.h>
#import <React/RCTEventEmitter.h>
RCT_EXTERN void RCTEnableAppearancePreference(BOOL enabled);
RCT_EXTERN void RCTOverrideAppearancePreference(NSString * /*colorSchemeOverride*/);
RCT_EXTERN void RCTUseKeyWindowForSystemStyle(BOOL useMainScreen);
RCT_EXTERN NSString *RCTCurrentOverrideAppearancePreference(void);
RCT_EXTERN NSString *RCTColorSchemePreference(UITraitCollection *traitCollection);
@interface RCTAppearance : RCTEventEmitter <RCTBridgeModule>
- (instancetype)init;
@end

View File

@@ -0,0 +1,162 @@
/*
* 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.
*/
#import "RCTAppearance.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConstants.h>
#import <React/RCTEventEmitter.h>
#import "CoreModulesPlugins.h"
using namespace facebook::react;
NSString *const RCTAppearanceColorSchemeLight = @"light";
NSString *const RCTAppearanceColorSchemeDark = @"dark";
static BOOL sAppearancePreferenceEnabled = YES;
void RCTEnableAppearancePreference(BOOL enabled)
{
sAppearancePreferenceEnabled = enabled;
}
static NSString *sColorSchemeOverride = nil;
void RCTOverrideAppearancePreference(NSString *const colorSchemeOverride)
{
sColorSchemeOverride = colorSchemeOverride;
}
static BOOL sUseKeyWindowForSystemStyle = NO;
void RCTUseKeyWindowForSystemStyle(BOOL useMainScreen)
{
sUseKeyWindowForSystemStyle = useMainScreen;
}
NSString *RCTCurrentOverrideAppearancePreference()
{
return sColorSchemeOverride;
}
NSString *RCTColorSchemePreference(UITraitCollection *traitCollection)
{
static NSDictionary *appearances;
static dispatch_once_t onceToken;
if (sColorSchemeOverride) {
return sColorSchemeOverride;
}
dispatch_once(&onceToken, ^{
appearances = @{
@(UIUserInterfaceStyleLight) : RCTAppearanceColorSchemeLight,
@(UIUserInterfaceStyleDark) : RCTAppearanceColorSchemeDark
};
});
if (!sAppearancePreferenceEnabled) {
// Return the default if the app doesn't allow different color schemes.
return RCTAppearanceColorSchemeLight;
}
if (!traitCollection) {
traitCollection = [UITraitCollection currentTraitCollection];
}
UIUserInterfaceStyle systemStyle = sUseKeyWindowForSystemStyle ? RCTKeyWindow().traitCollection.userInterfaceStyle
: traitCollection.userInterfaceStyle;
return appearances[@(systemStyle)] ?: RCTAppearanceColorSchemeLight;
}
@interface RCTAppearance () <NativeAppearanceSpec>
@end
@implementation RCTAppearance {
NSString *_currentColorScheme;
}
- (instancetype)init
{
if ((self = [super init])) {
UITraitCollection *traitCollection = RCTKeyWindow().traitCollection;
_currentColorScheme = RCTColorSchemePreference(traitCollection);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appearanceChanged:)
name:RCTUserInterfaceStyleDidChangeNotification
object:nil];
}
return self;
}
RCT_EXPORT_MODULE(Appearance)
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeAppearanceSpecJSI>(params);
}
RCT_EXPORT_METHOD(setColorScheme : (NSString *)style)
{
UIUserInterfaceStyle userInterfaceStyle = [RCTConvert UIUserInterfaceStyle:style];
NSMutableArray<UIWindow *> *windows = [NSMutableArray new];
for (UIWindowScene *scene in RCTSharedApplication().connectedScenes) {
[windows addObjectsFromArray:scene.windows];
}
for (UIWindow *window in windows) {
window.overrideUserInterfaceStyle = userInterfaceStyle;
}
}
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getColorScheme)
{
return _currentColorScheme;
}
- (void)appearanceChanged:(NSNotification *)notification
{
NSDictionary *userInfo = [notification userInfo];
UITraitCollection *traitCollection = nil;
if (userInfo) {
traitCollection = userInfo[RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey];
}
NSString *newColorScheme = RCTColorSchemePreference(traitCollection);
if (![_currentColorScheme isEqualToString:newColorScheme]) {
_currentColorScheme = newColorScheme;
[self sendEventWithName:@"appearanceChanged" body:@{@"colorScheme" : newColorScheme}];
}
}
#pragma mark - RCTEventEmitter
- (NSArray<NSString *> *)supportedEvents
{
return @[ @"appearanceChanged" ];
}
- (void)invalidate
{
[super invalidate];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
Class RCTAppearanceCls(void)
{
return RCTAppearance.class;
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
#import <React/RCTBridgeModule.h>
@interface RCTClipboard : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,51 @@
/*
* 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.
*/
#import "RCTClipboard.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <UIKit/UIKit.h>
#import "CoreModulesPlugins.h"
using namespace facebook::react;
@interface RCTClipboard () <NativeClipboardSpec>
@end
@implementation RCTClipboard
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
RCT_EXPORT_METHOD(setString : (NSString *)content)
{
UIPasteboard *clipboard = [UIPasteboard generalPasteboard];
clipboard.string = (content ?: @"");
}
RCT_EXPORT_METHOD(getString : (RCTPromiseResolveBlock)resolve reject : (__unused RCTPromiseRejectBlock)reject)
{
UIPasteboard *clipboard = [UIPasteboard generalPasteboard];
resolve((clipboard.string ?: @""));
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeClipboardSpecJSI>(params);
}
@end
Class RCTClipboardCls(void)
{
return RCTClipboard.class;
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
#import <React/RCTBridgeModule.h>
#import <React/RCTDevLoadingViewProtocol.h>
@interface RCTDevLoadingView : NSObject <RCTDevLoadingViewProtocol, RCTBridgeModule>
@end

View File

@@ -0,0 +1,314 @@
/*
* 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.
*/
#import <React/RCTDevLoadingView.h>
#include <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTAppearance.h>
#import <React/RCTBridge.h>
#import <React/RCTConstants.h>
#import <React/RCTConvert.h>
#import <React/RCTDefines.h>
#import <React/RCTDevLoadingViewSetEnabled.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
using namespace facebook::react;
@interface RCTDevLoadingView () <NativeDevLoadingViewSpec>
@end
#if RCT_DEV_MENU
@implementation RCTDevLoadingView {
UIWindow *_window;
UILabel *_label;
UIView *_container;
NSDate *_showDate;
BOOL _hiding;
dispatch_block_t _initialMessageBlock;
}
RCT_EXPORT_MODULE()
- (instancetype)init
{
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(hide)
name:RCTJavaScriptDidLoadNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(hide)
name:RCTJavaScriptDidFailToLoadNotification
object:nil];
}
return self;
}
+ (void)setEnabled:(BOOL)enabled
{
RCTDevLoadingViewSetEnabled(enabled);
}
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (void)clearInitialMessageDelay
{
if (_initialMessageBlock != nil) {
dispatch_block_cancel(_initialMessageBlock);
_initialMessageBlock = nil;
}
}
- (void)showInitialMessageDelayed:(void (^)())initialMessage
{
_initialMessageBlock = dispatch_block_create(static_cast<dispatch_block_flags_t>(0), initialMessage);
// We delay the initial loading message to prevent flashing it
// when loading progress starts quickly. To do that, we
// schedule the message to be shown in a block, and cancel
// the block later when the progress starts coming in.
// If the progress beats this timer, this message is not shown.
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), self->_initialMessageBlock);
}
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
{
if (!RCTDevLoadingViewGetEnabled() || _hiding) {
return;
}
// Input validation
if (message == nil || [message isEqualToString:@""]) {
NSLog(@"Error: message cannot be nil or empty");
return;
}
if (color == nil) {
NSLog(@"Error: color cannot be nil");
return;
}
if (backgroundColor == nil) {
NSLog(@"Error: backgroundColor cannot be nil");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (RCTRunningInTestEnvironment()) {
return;
}
self->_showDate = [NSDate date];
UIWindow *mainWindow = RCTKeyWindow();
self->_window = [[UIWindow alloc] initWithWindowScene:mainWindow.windowScene];
self->_window.windowLevel = UIWindowLevelStatusBar + 1;
self->_window.rootViewController = [UIViewController new];
self->_container = [[UIView alloc] init];
self->_container.backgroundColor = backgroundColor;
self->_container.translatesAutoresizingMaskIntoConstraints = NO;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)];
[self->_container addGestureRecognizer:tapGesture];
self->_container.userInteractionEnabled = YES;
self->_label = [[UILabel alloc] init];
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
self->_label.textAlignment = NSTextAlignmentCenter;
self->_label.textColor = color;
self->_label.text = message;
[self->_window.rootViewController.view addSubview:self->_container];
[self->_container addSubview:self->_label];
CGFloat topSafeAreaHeight = mainWindow.safeAreaInsets.top;
CGFloat height = topSafeAreaHeight + 25;
self->_window.frame = CGRectMake(0, 0, mainWindow.frame.size.width, height);
self->_window.hidden = NO;
[self->_window layoutIfNeeded];
[NSLayoutConstraint activateConstraints:@[
// Container constraints
[self->_container.topAnchor constraintEqualToAnchor:self->_window.rootViewController.view.topAnchor],
[self->_container.leadingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.leadingAnchor],
[self->_container.trailingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.trailingAnchor],
[self->_container.heightAnchor constraintEqualToConstant:height],
// Label constraints
[self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor],
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
]];
});
}
RCT_EXPORT_METHOD(
showMessage : (NSString *)message withColor : (NSNumber *__nonnull)color withBackgroundColor : (NSNumber *__nonnull)
backgroundColor)
{
[self showMessage:message color:[RCTConvert UIColor:color] backgroundColor:[RCTConvert UIColor:backgroundColor]];
}
RCT_EXPORT_METHOD(hide)
{
if (!RCTDevLoadingViewGetEnabled()) {
return;
}
// Cancel the initial message block so it doesn't display later and get stuck.
[self clearInitialMessageDelay];
dispatch_async(dispatch_get_main_queue(), ^{
self->_hiding = YES;
const NSTimeInterval MIN_PRESENTED_TIME = 0.6;
NSTimeInterval presentedTime = [[NSDate date] timeIntervalSinceDate:self->_showDate];
NSTimeInterval delay = MAX(0, MIN_PRESENTED_TIME - presentedTime);
CGRect windowFrame = self->_window.frame;
[UIView animateWithDuration:0.25
delay:delay
options:0
animations:^{
self->_window.frame = CGRectOffset(windowFrame, 0, -windowFrame.size.height);
}
completion:^(__unused BOOL finished) {
self->_window.frame = windowFrame;
self->_window.hidden = YES;
self->_window = nil;
self->_hiding = false;
}];
});
}
- (void)showProgressMessage:(NSString *)message
{
if (_window != nil) {
// This is an optimization. Since the progress can come in quickly,
// we want to do the minimum amount of work to update the UI,
// which is to only update the label text.
_label.text = message;
return;
}
UIColor *color = [UIColor whiteColor];
UIColor *backgroundColor = [UIColor colorWithHue:105 saturation:0 brightness:.25 alpha:1];
if ([self isDarkModeEnabled]) {
color = [UIColor colorWithHue:208 saturation:0.03 brightness:.14 alpha:1];
backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.98 alpha:1];
}
[self showMessage:message color:color backgroundColor:backgroundColor];
}
- (void)showOfflineMessage
{
UIColor *color = [UIColor whiteColor];
UIColor *backgroundColor = [UIColor blackColor];
if ([self isDarkModeEnabled]) {
color = [UIColor blackColor];
backgroundColor = [UIColor whiteColor];
}
NSString *message = [NSString stringWithFormat:@"Connect to %@ to develop JavaScript.", RCT_PACKAGER_NAME];
[self showMessage:message color:color backgroundColor:backgroundColor];
}
- (BOOL)isDarkModeEnabled
{
// We pass nil here to match the behavior of the native module.
// If we were to pass a view, then it's possible that this native
// banner would have a different color than the JavaScript banner
// (which always passes nil). This would result in an inconsistent UI.
return [RCTColorSchemePreference(nil) isEqualToString:@"dark"];
}
- (void)showWithURL:(NSURL *)URL
{
if (URL.fileURL) {
// If dev mode is not enabled, we don't want to show this kind of notification.
#if !RCT_DEV
return;
#endif
[self showOfflineMessage];
} else {
[self showInitialMessageDelayed:^{
NSString *message = [NSString stringWithFormat:@"Loading from %@\u2026", RCT_PACKAGER_NAME];
[self showProgressMessage:message];
}];
}
}
- (void)updateProgress:(RCTLoadingProgress *)progress
{
if (!progress) {
return;
}
// Cancel the initial message block so it's not flashed before progress.
[self clearInitialMessageDelay];
dispatch_async(dispatch_get_main_queue(), ^{
[self showProgressMessage:[progress description]];
});
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeDevLoadingViewSpecJSI>(params);
}
@end
#else
@implementation RCTDevLoadingView
+ (NSString *)moduleName
{
return nil;
}
+ (void)setEnabled:(BOOL)enabled
{
}
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor
{
}
- (void)showMessage:(NSString *)message withColor:(NSNumber *)color withBackgroundColor:(NSNumber *)backgroundColor
{
}
- (void)showWithURL:(NSURL *)URL
{
}
- (void)updateProgress:(RCTLoadingProgress *)progress
{
}
- (void)hide
{
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeDevLoadingViewSpecJSI>(params);
}
@end
#endif
Class RCTDevLoadingViewCls(void)
{
return RCTDevLoadingView.class;
}

View File

@@ -0,0 +1,148 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTBridgeProxy.h>
#import <React/RCTDefines.h>
RCT_EXTERN NSString *const RCTShowDevMenuNotification;
@interface RCTDevMenuConfiguration : NSObject
#if RCT_DEV_MENU
@property (nonatomic, readonly) BOOL devMenuEnabled;
@property (nonatomic, readonly) BOOL shakeGestureEnabled;
@property (nonatomic, readonly) BOOL keyboardShortcutsEnabled;
- (instancetype)initWithDevMenuEnabled:(BOOL)devMenuEnabled
shakeGestureEnabled:(BOOL)shakeGestureEnabled
keyboardShortcutsEnabled:(BOOL)keyboardShortcutsEnabled;
#endif
+ (instancetype)defaultConfiguration;
@end
@class RCTDevMenuItem;
/**
* Developer menu, useful for exposing extra functionality when debugging.
*/
@interface RCTDevMenu : NSObject
/**
* Deprecated, use RCTDevSettings instead.
*/
@property (nonatomic, assign) BOOL shakeToShow DEPRECATED_ATTRIBUTE;
/**
* Deprecated, use RCTDevSettings instead.
*/
@property (nonatomic, assign) BOOL profilingEnabled DEPRECATED_ATTRIBUTE;
/**
* Deprecated, use RCTDevSettings instead.
*/
@property (nonatomic, assign) BOOL hotLoadingEnabled DEPRECATED_ATTRIBUTE;
/**
* Whether the hotkeys that toggles the developer menu is enabled.
*/
@property (nonatomic, assign) BOOL hotkeysEnabled;
/**
* Whether the developer menu is enabled.
*/
@property (nonatomic, assign) BOOL devMenuEnabled;
/**
* Whether keyboard shortcuts are enabled.
*/
@property (nonatomic, assign) BOOL keyboardShortcutsEnabled;
/**
* Presented items in development menu
*/
@property (nonatomic, copy, readonly) NSArray<RCTDevMenuItem *> *presentedItems;
/**
* Detect if actions sheet (development menu) is shown
*/
- (BOOL)isActionSheetShown;
/**
* Manually show the dev menu (can be called from JS).
*/
- (void)show;
/**
* Deprecated, use `RCTReloadCommand` instead.
*/
- (void)reload DEPRECATED_ATTRIBUTE;
/**
* Deprecated. Use the `-addItem:` method instead.
*/
- (void)addItem:(NSString *)title handler:(void (^)(void))handler DEPRECATED_ATTRIBUTE;
/**
* Add custom item to the development menu. The handler will be called
* when user selects the item.
*/
- (void)addItem:(RCTDevMenuItem *)item;
/**
* Disable the reload command (Cmd+R) in the simulator.
*/
- (void)disableReloadCommand;
@end
typedef NSString * (^RCTDevMenuItemTitleBlock)(void);
/**
* Developer menu item, used to expose additional functionality via the menu.
*/
@interface RCTDevMenuItem : NSObject
/**
* This creates an item with a simple push-button interface, used to trigger an
* action.
*/
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(dispatch_block_t)handler;
/**
* This creates an item with a simple push-button interface, used to trigger an
* action. getTitleForPresentation is called each time the item is about to be
* presented, and should return the item's title.
*/
+ (instancetype)buttonItemWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock handler:(dispatch_block_t)handler;
@property (nonatomic, assign, getter=isDisabled) BOOL disabled;
@end
/**
* This category makes the developer menu instance available via the
* RCTBridge, which is useful for any class that needs to access the menu.
*/
@interface RCTBridge (RCTDevMenu)
@property (nonatomic, readonly) RCTDevMenu *devMenu;
@end
@interface RCTBridgeProxy (RCTDevMenu)
@property (nonatomic, readonly) RCTDevMenu *devMenu;
@end

View File

@@ -0,0 +1,649 @@
/*
* 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.
*/
#import <React/RCTDevMenu.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTDefines.h>
#import <React/RCTDevSettings.h>
#import <React/RCTKeyCommands.h>
#import <React/RCTLog.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
#if RCT_DEV_MENU
#if RCT_ENABLE_INSPECTOR
#import <React/RCTInspectorDevServerHelper.h>
#endif
@protocol RCTDevMenuItemProvider
- (RCTDevMenuItem *)devMenuItem;
@end
NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
@implementation RCTDevMenuConfiguration
- (instancetype)initWithDevMenuEnabled:(BOOL)devMenuEnabled
shakeGestureEnabled:(BOOL)shakeGestureEnabled
keyboardShortcutsEnabled:(BOOL)keyboardShortcutsEnabled
{
if (self = [super init]) {
_devMenuEnabled = devMenuEnabled;
_shakeGestureEnabled = shakeGestureEnabled;
_keyboardShortcutsEnabled = keyboardShortcutsEnabled;
}
return self;
}
+ (instancetype)defaultConfiguration
{
return [[self alloc] initWithDevMenuEnabled:RCT_DEV_MENU
shakeGestureEnabled:RCT_DEV_MENU
keyboardShortcutsEnabled:RCT_DEV_MENU];
}
@end
@implementation UIWindow (RCTDevMenu)
- (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
}
}
@end
@implementation RCTDevMenuItem {
RCTDevMenuItemTitleBlock _titleBlock;
dispatch_block_t _handler;
}
- (instancetype)initWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock handler:(dispatch_block_t)handler
{
if ((self = [super init]) != nullptr) {
_titleBlock = [titleBlock copy];
_handler = [handler copy];
}
return self;
}
RCT_NOT_IMPLEMENTED(-(instancetype)init)
+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(dispatch_block_t)handler
{
return [[self alloc] initWithTitleBlock:titleBlock handler:handler];
}
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(dispatch_block_t)handler
{
return [[self alloc]
initWithTitleBlock:^NSString * {
return title;
}
handler:handler];
}
- (void)callHandler
{
if (_handler != nullptr) {
_handler();
}
}
- (NSString *)title
{
if (_titleBlock != nullptr) {
return _titleBlock();
}
return nil;
}
@end
typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
@end
@implementation RCTDevMenu {
UIAlertController *_actionSheet;
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
}
@synthesize bridge = _bridge;
@synthesize moduleRegistry = _moduleRegistry;
@synthesize callableJSModules = _callableJSModules;
@synthesize bundleManager = _bundleManager;
RCT_EXPORT_MODULE()
+ (void)initialize
{
// We're swizzling here because it's poor form to override methods in a category,
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
// no need to call the original implementation.
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
}
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (instancetype)init
{
if ((self = [super init]) != nullptr) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showOnShake)
name:RCTShowDevMenuNotification
object:nil];
_extraMenuItems = [NSMutableArray new];
_keyboardShortcutsEnabled = true;
_devMenuEnabled = true;
[self registerHotkeys];
}
return self;
}
- (void)registerHotkeys
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
__weak __typeof(self) weakSelf = self;
// Toggle debug menu
[commands registerKeyCommandWithInput:@"d"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[weakSelf toggle];
}];
// Toggle element inspector
[commands registerKeyCommandWithInput:@"i"
modifierFlags:UIKeyModifierCommand
action:^(__unused UIKeyCommand *command) {
[(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"]
toggleElementInspector];
}];
#endif
}
- (void)unregisterHotkeys
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
[commands unregisterKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand];
[commands unregisterKeyCommandWithInput:@"i" modifierFlags:UIKeyModifierCommand];
#endif
}
- (BOOL)isHotkeysRegistered
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
return [commands isKeyCommandRegisteredForInput:@"d" modifierFlags:UIKeyModifierCommand] &&
[commands isKeyCommandRegisteredForInput:@"i" modifierFlags:UIKeyModifierCommand];
#else
return NO;
#endif
}
- (BOOL)isReloadCommandRegistered
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
return [commands isKeyCommandRegisteredForInput:@"r" modifierFlags:UIKeyModifierCommand];
#else
return NO;
#endif
}
- (void)unregisterReloadCommand
{
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
[commands unregisterKeyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand];
#endif
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
_presentedItems = nil;
[_actionSheet dismissViewControllerAnimated:YES
completion:^(void){
}];
}
- (void)showOnShake
{
if ([((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]) isShakeToShowDevMenuEnabled]) {
NSMutableArray<UIWindow *> *windows = [NSMutableArray new];
for (UIWindowScene *scene in RCTSharedApplication().connectedScenes) {
[windows addObjectsFromArray:scene.windows];
}
for (UIWindow *window in windows) {
NSString *recursiveDescription = [window valueForKey:@"recursiveDescription"];
if ([recursiveDescription containsString:@"RCTView"]) {
[self show];
return;
}
}
}
}
- (void)toggle
{
if (_actionSheet.isBeingPresented || _actionSheet.beingDismissed) {
return;
}
if (_actionSheet != nullptr) {
[_actionSheet dismissViewControllerAnimated:YES
completion:^(void) {
self->_actionSheet = nil;
}];
} else {
[self show];
}
}
- (BOOL)isActionSheetShown
{
return _actionSheet != nil;
}
- (void)addItem:(NSString *)title handler:(void (^)(void))handler
{
[self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
}
- (void)addItem:(RCTDevMenuItem *)item
{
[_extraMenuItems addObject:item];
}
- (void)setKeyboardShortcutsEnabled:(BOOL)keyboardShortcutsEnabled
{
if (_keyboardShortcutsEnabled != keyboardShortcutsEnabled) {
[self setHotkeysEnabled:keyboardShortcutsEnabled];
if (!keyboardShortcutsEnabled) {
[self disableReloadCommand];
}
}
}
- (void)setDefaultJSBundle
{
[[RCTBundleURLProvider sharedSettings] resetToDefaults];
self->_bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForFallbackExtension:nil];
RCTTriggerReloadCommandListeners(@"Dev menu - reset to default");
}
- (NSArray<RCTDevMenuItem *> *)_menuItemsToPresent
{
NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
// Add built-in items
__weak RCTDevSettings *devSettings = [_moduleRegistry moduleForName:"DevSettings"];
__weak RCTDevMenu *weakSelf = self;
__weak RCTBundleManager *bundleManager = _bundleManager;
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload"
handler:^{
RCTTriggerReloadCommandListeners(@"Dev menu - reload");
}]];
if (!devSettings.isProfilingEnabled) {
#if RCT_ENABLE_INSPECTOR
if (devSettings.isDeviceDebuggingAvailable) {
// On-device JS debugging (CDP). Render action to open debugger frontend.
BOOL isDisconnected = RCTInspectorDevServerHelper.isPackagerDisconnected;
NSString *title = isDisconnected
? [NSString stringWithFormat:@"Connect to %@ to debug JavaScript", RCT_PACKAGER_NAME]
: @"Open DevTools";
RCTDevMenuItem *item = [RCTDevMenuItem
buttonItemWithTitle:title
handler:^{
[RCTInspectorDevServerHelper
openDebugger:bundleManager.bundleURL
withErrorMessage:
@"Failed to open debugger. Please check that the dev server is running and reload the app."];
}];
[item setDisabled:isDisconnected];
[items addObject:item];
}
#endif
}
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Toggle Element Inspector";
}
handler:^{
[devSettings toggleElementInspector];
}]];
if (devSettings.isHotLoadingAvailable) {
[items addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
// Previously known as "Hot Reloading". We won't use this term anymore.
return devSettings.isHotLoadingEnabled ? @"Disable Fast Refresh" : @"Enable Fast Refresh";
}
handler:^{
devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
}]];
}
id perfMonitorItemOpaque = [_moduleRegistry moduleForName:"PerfMonitor"];
SEL devMenuItem = @selector(devMenuItem);
if ([perfMonitorItemOpaque respondsToSelector:devMenuItem]) {
RCTDevMenuItem *perfMonitorItem = [perfMonitorItemOpaque devMenuItem];
[items addObject:perfMonitorItem];
}
[items
addObject:[RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return @"Configure Bundler";
}
handler:^{
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:@"Configure Bundler"
message:@"Provide a custom bundler address, port, and entrypoint."
preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"0.0.0.0";
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"8081";
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"index";
}];
[alertController
addAction:[UIAlertAction
actionWithTitle:@"Apply Changes"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
NSArray *textfields = alertController.textFields;
UITextField *ipTextField = textfields[0];
UITextField *portTextField = textfields[1];
UITextField *bundleRootTextField = textfields[2];
NSString *bundleRoot = bundleRootTextField.text;
if (ipTextField.text.length == 0 && portTextField.text.length == 0) {
[weakSelf setDefaultJSBundle];
return;
}
NSNumberFormatter *formatter = [NSNumberFormatter new];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
NSNumber *portNumber =
[formatter numberFromString:portTextField.text];
if (portNumber == nil) {
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
}
[RCTBundleURLProvider sharedSettings].jsLocation = [NSString
stringWithFormat:@"%@:%d", ipTextField.text, portNumber.intValue];
if (bundleRoot.length == 0) {
[bundleManager resetBundleURL];
} else {
bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings]
jsBundleURLForBundleRoot:bundleRoot];
}
RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Reset to Default"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *action) {
[weakSelf setDefaultJSBundle];
}]];
UIAlertAction *configCancelAction =
[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:^(__unused UIAlertAction *action) {
return;
}];
[configCancelAction setValue:[UIColor systemRedColor] forKey:@"titleTextColor"];
[alertController addAction:configCancelAction];
[RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
}]];
[items addObjectsFromArray:_extraMenuItems];
return items;
}
RCT_EXPORT_METHOD(show)
{
if ((_actionSheet != nullptr) || RCTRunningInAppExtension() || !_devMenuEnabled) {
return;
}
// On larger devices we don't have an anchor point for the action sheet
UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone
? UIAlertControllerStyleActionSheet
: UIAlertControllerStyleAlert;
_actionSheet = [UIAlertController alertControllerWithTitle:@"React Native Dev Menu" message:nil preferredStyle:style];
NSAttributedString *title =
[[NSAttributedString alloc] initWithString:@"React Native Dev Menu"
attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:17]}];
[_actionSheet setValue:title forKey:@"_attributedTitle"];
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
for (RCTDevMenuItem *item in items) {
UIAlertAction *action = [UIAlertAction actionWithTitle:item.title
style:UIAlertActionStyleDefault
handler:[self alertActionHandlerForDevItem:item]];
[action setEnabled:!item.isDisabled];
[_actionSheet addAction:action];
}
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:[self alertActionHandlerForDevItem:nil]];
[cancelAction setValue:[UIColor systemRedColor] forKey:@"titleTextColor"];
[_actionSheet addAction:cancelAction];
_presentedItems = items;
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
[_callableJSModules invokeModule:@"RCTNativeAppEventEmitter" method:@"emit" withArgs:@[ @"RCTDevMenuShown" ]];
}
- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
{
return ^(__unused UIAlertAction *action) {
if (item != nullptr) {
[item callHandler];
}
self->_actionSheet = nil;
};
}
#pragma mark - deprecated methods and properties
#define WARN_DEPRECATED_DEV_MENU_EXPORT() \
RCTLogWarn(@"Using deprecated method %s, use RCTDevSettings instead", __func__)
- (void)setShakeToShow:(BOOL)shakeToShow
{
[[_moduleRegistry moduleForName:"DevSettings"] setIsShakeToShowDevMenuEnabled:shakeToShow];
}
- (BOOL)shakeToShow
{
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isShakeToShowDevMenuEnabled;
}
RCT_EXPORT_METHOD(reload)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
RCTTriggerReloadCommandListeners(@"Unknown from JS");
}
RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled = enabled;
}
- (BOOL)profilingEnabled
{
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled;
}
RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled)
{
WARN_DEPRECATED_DEV_MENU_EXPORT();
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled = enabled;
}
- (BOOL)hotLoadingEnabled
{
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled;
}
- (void)setHotkeysEnabled:(BOOL)enabled
{
if (enabled) {
[self registerHotkeys];
} else {
[self unregisterHotkeys];
}
}
- (BOOL)hotkeysEnabled
{
return [self isHotkeysRegistered];
}
- (void)disableReloadCommand
{
if ([self isReloadCommandRegistered]) {
[self unregisterReloadCommand];
}
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(params);
}
@end
#else // Unavailable when not in dev mode
@interface RCTDevMenu () <NativeDevMenuSpec>
@end
@implementation RCTDevMenuConfiguration
+ (instancetype)defaultConfiguration
{
return nil;
}
@end
@implementation RCTDevMenu
- (void)show
{
}
- (void)reload
{
}
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler
{
}
- (void)addItem:(RCTDevMenu *)item
{
}
- (void)disableReloadCommand
{
}
- (BOOL)isActionSheetShown
{
return NO;
}
+ (NSString *)moduleName
{
return @"DevMenu";
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(params);
}
@end
@implementation RCTDevMenuItem
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(void (^)(void))handler
{
return nil;
}
+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(void (^)(void))handler
{
return nil;
}
@end
#endif
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
#if RCT_DEV_MENU
return [self moduleForClass:[RCTDevMenu class]];
#else
return nil;
#endif
}
@end
@implementation RCTBridgeProxy (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
#if RCT_DEV_MENU
return [self moduleForClass:[RCTDevMenu class]];
#else
return nil;
#endif
}
@end
Class RCTDevMenuCls(void)
{
return RCTDevMenu.class;
}

View File

@@ -0,0 +1,23 @@
/*
* 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.
*/
#import <React/RCTBridgeModule.h>
@class RCTDevMenuConfiguration;
@interface RCTDevMenuConfigurationDecorator : NSObject
#if RCT_DEV_MENU
@property (nonatomic, strong, readonly) RCTDevMenuConfiguration *__nullable devMenuConfiguration;
- (instancetype)initWithDevMenuConfiguration:(RCTDevMenuConfiguration *__nullable)devMenuConfiguration;
- (void)decorate:(id<RCTBridgeModule>)devMenuModule;
#endif
@end

View File

@@ -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.
*/
#import "RCTDevMenuConfigurationDecorator.h"
#if RCT_DEV_MENU
#import <React/RCTDevMenu.h>
#import <React/RCTDevSettings.h>
@implementation RCTDevMenuConfigurationDecorator
- (instancetype)initWithDevMenuConfiguration:(RCTDevMenuConfiguration *__nullable)devMenuConfiguration
{
if (self = [super init]) {
_devMenuConfiguration = devMenuConfiguration;
}
return self;
}
- (void)decorate:(id<RCTBridgeModule>)bridgeModule
{
if (_devMenuConfiguration == nil) {
return;
}
if ([bridgeModule isKindOfClass:[RCTDevMenu class]]) {
RCTDevMenu *devMenu = (RCTDevMenu *)bridgeModule;
devMenu.devMenuEnabled = _devMenuConfiguration.devMenuEnabled;
devMenu.keyboardShortcutsEnabled = _devMenuConfiguration.keyboardShortcutsEnabled;
}
if ([bridgeModule isKindOfClass:[RCTDevSettings class]]) {
RCTDevSettings *devSettings = (RCTDevSettings *)bridgeModule;
devSettings.isShakeGestureEnabled = _devMenuConfiguration.shakeGestureEnabled;
}
}
@end
#else
@implementation RCTDevMenuConfigurationDecorator : NSObject
@end
#endif

View File

@@ -0,0 +1,125 @@
/*
* 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.
*/
#import <React/RCTBridge.h>
#import <React/RCTBridgeProxy.h>
#import <React/RCTDefines.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTInitializing.h>
@protocol RCTPackagerClientMethod;
/**
* An abstraction for a key-value store to manage RCTDevSettings behavior.
* The default implementation persists settings using NSUserDefaults.
*/
@protocol RCTDevSettingsDataSource <NSObject>
/**
* Updates the setting with the given key to the given value.
* How the data source's state changes depends on the implementation.
*/
- (void)updateSettingWithValue:(id)value forKey:(NSString *)key;
/**
* Returns the value for the setting with the given key.
*/
- (id)settingForKey:(NSString *)key;
@end
@protocol RCTDevSettingsInspectable <NSObject>
/**
* Whether current jsi::Runtime is inspectable.
* Only set when using as a bridgeless turbo module.
*/
@property (nonatomic, assign, readwrite) BOOL isInspectable;
@end
@interface RCTDevSettings : RCTEventEmitter <RCTInitializing>
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource;
@property (nonatomic, readonly) BOOL isHotLoadingAvailable;
@property (nonatomic, readonly) BOOL isDeviceDebuggingAvailable;
/*
* Whether shaking will show RCTDevMenu. The menu is enabled by default if RCT_DEV=1, but
* you may wish to disable it so that you can provide your own shake handler.
*/
@property (nonatomic, assign) BOOL isShakeToShowDevMenuEnabled;
/**
* Whether performance profiling is enabled.
*/
@property (nonatomic, assign, setter=setProfilingEnabled:) BOOL isProfilingEnabled;
/**
* Whether hot loading is enabled.
*/
@property (nonatomic, assign, setter=setHotLoadingEnabled:) BOOL isHotLoadingEnabled;
/**
* Whether shake gesture is enabled.
*/
@property (nonatomic, assign) BOOL isShakeGestureEnabled;
/**
* Enables starting of profiling sampler on launch
*/
@property (nonatomic, assign) BOOL startSamplingProfilerOnLaunch;
/**
* Whether the element inspector is visible.
*/
@property (nonatomic, readonly) BOOL isElementInspectorShown;
/**
* Whether the performance monitor is visible.
*/
@property (nonatomic, assign) BOOL isPerfMonitorShown;
/**
* Toggle the element inspector.
*/
- (void)toggleElementInspector;
/**
* Set up the HMRClient if loading the bundle from Metro.
*/
- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL;
/**
* Register additional bundles with the HMRClient.
*/
- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL;
#if RCT_DEV_MENU
- (void)addHandler:(id<RCTPackagerClientMethod>)handler
forPackagerMethod:(NSString *)name __deprecated_msg("Use RCTPackagerConnection directly instead");
#endif
@end
@interface RCTBridge (RCTDevSettings)
@property (nonatomic, readonly) RCTDevSettings *devSettings;
@end
@interface RCTBridgeProxy (RCTDevSettings)
@property (nonatomic, readonly) RCTDevSettings *devSettings;
@end
// In debug builds, the dev menu is enabled by default but it is further customizable using this method.
// However, this method only has an effect in builds where the dev menu is actually compiled in.
// (i.e. RCT_DEV or RCT_DEV_MENU is set)
RCT_EXTERN void RCTDevSettingsSetEnabled(BOOL enabled);

View File

@@ -0,0 +1,618 @@
/*
* 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.
*/
#import "RCTDevSettings.h"
#import <objc/runtime.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTBridge+Inspector.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTConstants.h>
#import <React/RCTDevMenu.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTLog.h>
#import <React/RCTProfile.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTUtils.h>
#import <atomic>
#import "CoreModulesPlugins.h"
static NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled";
static NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled";
static NSString *const kRCTDevSettingIsInspectorShown = @"showInspector";
static NSString *const kRCTDevSettingShakeToShowDevMenu = @"shakeToShow";
static NSString *const kRCTDevSettingIsPerfMonitorShown = @"RCTPerfMonitorKey";
static NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu";
#if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION
#import <React/RCTPackagerClient.h>
#import <React/RCTPackagerConnection.h>
#endif
#if RCT_ENABLE_INSPECTOR
#import <React/RCTInspectorDevServerHelper.h>
#endif
#if RCT_DEV
static BOOL devSettingsMenuEnabled = YES;
#else
static BOOL devSettingsMenuEnabled = NO;
#endif
void RCTDevSettingsSetEnabled(BOOL enabled)
{
devSettingsMenuEnabled = enabled;
}
#if RCT_DEV_MENU || RCT_REMOTE_PROFILE
@interface RCTDevSettingsUserDefaultsDataSource : NSObject <RCTDevSettingsDataSource>
@end
@implementation RCTDevSettingsUserDefaultsDataSource {
NSMutableDictionary *_settings;
NSUserDefaults *_userDefaults;
}
- (instancetype)init
{
return [self initWithDefaultValues:nil];
}
- (instancetype)initWithDefaultValues:(NSDictionary *)defaultValues
{
if (self = [super init]) {
_userDefaults = [NSUserDefaults standardUserDefaults];
if (defaultValues) {
[self _reloadWithDefaults:defaultValues];
}
}
return self;
}
- (void)updateSettingWithValue:(id)value forKey:(NSString *)key
{
RCTAssert((key != nil), @"%@", [NSString stringWithFormat:@"%@: Tried to update nil key", [self class]]);
id currentValue = [self settingForKey:key];
if (currentValue == value || [currentValue isEqual:value]) {
return;
}
if (value) {
_settings[key] = value;
} else {
[_settings removeObjectForKey:key];
}
[_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey];
}
- (id)settingForKey:(NSString *)key
{
return _settings[key];
}
- (void)_reloadWithDefaults:(NSDictionary *)defaultValues
{
NSDictionary *existingSettings = [_userDefaults objectForKey:kRCTDevSettingsUserDefaultsKey];
_settings = existingSettings ? [existingSettings mutableCopy] : [NSMutableDictionary dictionary];
for (NSString *key in [defaultValues keyEnumerator]) {
if (!_settings[key]) {
_settings[key] = defaultValues[key];
}
}
[_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey];
}
@end
#if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION
static RCTHandlerToken reloadToken;
static RCTHandlerToken devMenuToken;
static std::atomic<int> numInitializedModules{0};
#endif
@interface RCTDevSettings () <RCTBridgeModule, RCTInvalidating, NativeDevSettingsSpec, RCTDevSettingsInspectable> {
BOOL _isJSLoaded;
}
@property (nonatomic, strong) Class executorClass;
@property (nonatomic, readwrite, strong) id<RCTDevSettingsDataSource> dataSource;
@end
@implementation RCTDevSettings
@synthesize isInspectable = _isInspectable;
@synthesize bundleManager = _bundleManager;
RCT_EXPORT_MODULE()
- (instancetype)init
{
// Default behavior is to use NSUserDefaults with shake and hot loading enabled.
NSDictionary *defaultValues = @{
kRCTDevSettingShakeToShowDevMenu : @YES,
kRCTDevSettingHotLoadingEnabled : @YES,
};
RCTDevSettingsUserDefaultsDataSource *dataSource =
[[RCTDevSettingsUserDefaultsDataSource alloc] initWithDefaultValues:defaultValues];
_isShakeGestureEnabled = true;
return [self initWithDataSource:dataSource];
}
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (BOOL)_isBridgeMode
{
return [self.bridge isKindOfClass:[RCTBridge class]];
}
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource
{
if (self = [super init]) {
_dataSource = dataSource;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(jsLoaded:)
name:RCTJavaScriptDidLoadNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(jsLoaded:)
name:@"RCTInstanceDidLoadBundle"
object:nil];
}
return self;
}
- (void)initialize
{
#if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION
if (numInitializedModules++ == 0) {
reloadToken = [[RCTPackagerConnection sharedPackagerConnection]
addNotificationHandler:^(id params) {
RCTTriggerReloadCommandListeners(@"Global hotkey");
}
queue:dispatch_get_main_queue()
forMethod:@"reload"];
#if RCT_DEV_MENU
devMenuToken = [[RCTPackagerConnection sharedPackagerConnection]
addNotificationHandler:^(id params) {
[[self.moduleRegistry moduleForName:"DevMenu"] show];
}
queue:dispatch_get_main_queue()
forMethod:@"devMenu"];
#endif
}
#endif
#if RCT_ENABLE_INSPECTOR
if ([self _isBridgeMode]) {
// We need this dispatch to the main thread because the bridge is not yet
// finished with its initialisation. By the time it relinquishes control of
// the main thread, this operation can be performed.
__weak __typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
id dispatchBlock = ^{
__typeof(self) strongSelf2 = weakSelf;
if (!strongSelf2) {
return;
}
NSURL *url = strongSelf2.bundleManager.bundleURL;
[RCTInspectorDevServerHelper connectWithBundleURL:url];
};
[strongSelf.bridge dispatchBlock:dispatchBlock queue:RCTJSThread];
});
} else {
NSURL *url = self.bundleManager.bundleURL;
[RCTInspectorDevServerHelper connectWithBundleURL:url];
}
#endif
__weak __typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf _synchronizeAllSettings];
});
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
[super invalidate];
#if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION
if (--numInitializedModules == 0) {
[[RCTPackagerConnection sharedPackagerConnection] removeHandler:reloadToken];
#if RCT_DEV_MENU
[[RCTPackagerConnection sharedPackagerConnection] removeHandler:devMenuToken];
#endif
}
#endif
}
- (NSArray<NSString *> *)supportedEvents
{
return @[ @"didPressMenuItem" ];
}
- (void)_updateSettingWithValue:(id)value forKey:(NSString *)key
{
[_dataSource updateSettingWithValue:value forKey:key];
}
- (id)settingForKey:(NSString *)key
{
return [_dataSource settingForKey:key];
}
- (void)setIsShakeGestureEnabled:(BOOL)isShakeGestureEnabled
{
_isShakeGestureEnabled = isShakeGestureEnabled;
[self setIsShakeToShowDevMenuEnabled:isShakeGestureEnabled];
}
- (BOOL)isDeviceDebuggingAvailable
{
#if RCT_ENABLE_INSPECTOR
if ([self _isBridgeMode]) {
return self.bridge.isInspectable;
} else {
return self.isInspectable;
}
#else
return false;
#endif // RCT_ENABLE_INSPECTOR
}
- (BOOL)isHotLoadingAvailable
{
if (self.bundleManager.bundleURL) {
return !self.bundleManager.bundleURL.fileURL;
}
return NO;
}
RCT_EXPORT_METHOD(reload)
{
RCTTriggerReloadCommandListeners(@"Unknown From JS");
}
RCT_EXPORT_METHOD(reloadWithReason : (NSString *)reason)
{
RCTTriggerReloadCommandListeners(reason);
}
RCT_EXPORT_METHOD(onFastRefresh)
{
[self.bridge onFastRefresh];
}
RCT_EXPORT_METHOD(setIsShakeToShowDevMenuEnabled : (BOOL)enabled)
{
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingShakeToShowDevMenu];
}
- (BOOL)isShakeToShowDevMenuEnabled
{
return _isShakeGestureEnabled && [[self settingForKey:kRCTDevSettingShakeToShowDevMenu] boolValue];
}
RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled)
{
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingProfilingEnabled];
[self _profilingSettingDidChange];
}
- (BOOL)isProfilingEnabled
{
return [[self settingForKey:kRCTDevSettingProfilingEnabled] boolValue];
}
- (void)_profilingSettingDidChange
{
BOOL enabled = self.isProfilingEnabled;
if (self.isHotLoadingAvailable && enabled != RCTProfileIsProfiling()) {
if (enabled) {
[self.bridge startProfiling];
} else {
__weak __typeof(self) weakSelf = self;
[self.bridge stopProfiling:^(NSData *logData) {
__typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
RCTProfileSendResult(strongSelf.bridge, @"systrace", logData);
}];
}
}
}
RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled)
{
if (self.isHotLoadingEnabled != enabled) {
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingHotLoadingEnabled];
if (_isJSLoaded) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if (enabled) {
[self.callableJSModules invokeModule:@"HMRClient" method:@"enable" withArgs:@[]];
} else {
[self.callableJSModules invokeModule:@"HMRClient" method:@"disable" withArgs:@[]];
}
#pragma clang diagnostic pop
}
}
}
- (BOOL)isHotLoadingEnabled
{
return [[self settingForKey:kRCTDevSettingHotLoadingEnabled] boolValue];
}
RCT_EXPORT_METHOD(toggleElementInspector)
{
BOOL value = [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
[self _updateSettingWithValue:@(!value) forKey:kRCTDevSettingIsInspectorShown];
if (_isJSLoaded) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[self.moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"toggleElementInspector" body:nil];
#pragma clang diagnostic pop
}
}
RCT_EXPORT_METHOD(addMenuItem : (NSString *)title)
{
__weak __typeof(self) weakSelf = self;
[(RCTDevMenu *)[self.moduleRegistry moduleForName:"DevMenu"]
addItem:[RCTDevMenuItem buttonItemWithTitle:title
handler:^{
[weakSelf sendEventWithName:@"didPressMenuItem" body:@{@"title" : title}];
}]];
}
- (BOOL)isElementInspectorShown
{
return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
}
- (void)setIsPerfMonitorShown:(BOOL)isPerfMonitorShown
{
[self _updateSettingWithValue:@(isPerfMonitorShown) forKey:kRCTDevSettingIsPerfMonitorShown];
}
- (BOOL)isPerfMonitorShown
{
return [[self settingForKey:kRCTDevSettingIsPerfMonitorShown] boolValue];
}
- (void)setExecutorClass:(Class)executorClass
{
_executorClass = executorClass;
if (self.bridge.executorClass != executorClass) {
// TODO (6929129): we can remove this special case test once we have better
// support for custom executors in the dev menu. But right now this is
// needed to prevent overriding a custom executor with the default if a
// custom executor has been set directly on the bridge
if (executorClass == Nil) {
return;
}
self.bridge.executorClass = executorClass;
RCTTriggerReloadCommandListeners(@"Custom executor class reset");
}
}
- (void)addHandler:(id<RCTPackagerClientMethod>)handler forPackagerMethod:(NSString *)name
{
#if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION
[[RCTPackagerConnection sharedPackagerConnection] addHandler:handler forMethod:name];
#endif
}
- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL
{
if (bundleURL && !bundleURL.fileURL) {
NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:bundleURL resolvingAgainstBaseURL:NO];
NSString *const path = [urlComponents.path substringFromIndex:1]; // Strip initial slash.
NSString *const host = urlComponents.host;
NSNumber *const port = urlComponents.port;
NSString *const scheme = urlComponents.scheme;
BOOL isHotLoadingEnabled = self.isHotLoadingEnabled;
[self.callableJSModules
invokeModule:@"HMRClient"
method:@"setup"
withArgs:@[ RCTPlatformName, path, host, RCTNullIfNil(port), @(isHotLoadingEnabled), scheme ]];
}
}
- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL
{
if (bundleURL && !bundleURL.fileURL) { // isHotLoadingAvailable check
[self.callableJSModules invokeModule:@"HMRClient"
method:@"registerBundle"
withArgs:@[ [bundleURL absoluteString] ]];
}
}
RCT_EXPORT_METHOD(openDebugger)
{
#if RCT_ENABLE_INSPECTOR
[RCTInspectorDevServerHelper
openDebugger:self.bundleManager.bundleURL
withErrorMessage:@"Failed to open debugger. Please check that the dev server is running and reload the app."];
#endif
}
#pragma mark - Internal
/**
* Query the data source for all possible settings and make sure we're doing the right
* thing for the state of each setting.
*/
- (void)_synchronizeAllSettings
{
[self _profilingSettingDidChange];
}
- (void)jsLoaded:(NSNotification *)notification
{
// In bridge mode, the bridge that sent the notif must be the same as the one stored in this module.
// In bridgless mode, we don't care about this.
if ([notification.name isEqualToString:RCTJavaScriptDidLoadNotification] &&
notification.userInfo[@"bridge"] != self.bridge) {
return;
}
_isJSLoaded = YES;
__weak __typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// update state again after the bridge has finished loading
[strongSelf _synchronizeAllSettings];
// Inspector can only be shown after JS has loaded
if ([strongSelf isElementInspectorShown]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[strongSelf.moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"toggleElementInspector"
body:nil];
#pragma clang diagnostic pop
}
});
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeDevSettingsSpecJSI>(params);
}
@end
#else // #if RCT_DEV_MENU
@interface RCTDevSettings () <NativeDevSettingsSpec>
@end
@implementation RCTDevSettings
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource
{
return [super init];
}
- (void)initialize
{
}
- (BOOL)isHotLoadingAvailable
{
return NO;
}
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (id)settingForKey:(NSString *)key
{
return nil;
}
- (void)reload
{
}
- (void)reloadWithReason:(NSString *)reason
{
}
- (void)onFastRefresh
{
}
- (void)setHotLoadingEnabled:(BOOL)isHotLoadingEnabled
{
}
- (void)setProfilingEnabled:(BOOL)isProfilingEnabled
{
}
- (void)toggleElementInspector
{
}
- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL
{
}
- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL
{
}
- (void)openDebugger
{
}
- (void)addMenuItem:(NSString *)title
{
}
- (void)setIsShakeToShowDevMenuEnabled:(BOOL)enabled
{
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeDevSettingsSpecJSI>(params);
}
@end
#endif // #if RCT_DEV_MENU
@implementation RCTBridge (RCTDevSettings)
- (RCTDevSettings *)devSettings
{
#if RCT_REMOTE_PROFILE
return [self moduleForClass:[RCTDevSettings class]];
#elif RCT_DEV_MENU
return devSettingsMenuEnabled ? [self moduleForClass:[RCTDevSettings class]] : nil;
#else
return nil;
#endif
}
@end
@implementation RCTBridgeProxy (RCTDevSettings)
- (RCTDevSettings *)devSettings
{
#if RCT_REMOTE_PROFILE
return [self moduleForClass:[RCTDevSettings class]];
#elif RCT_DEV_MENU
return devSettingsMenuEnabled ? [self moduleForClass:[RCTDevSettings class]] : nil;
#else
return nil;
#endif
}
@end
Class RCTDevSettingsCls(void)
{
return RCTDevSettings.class;
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
#import <Foundation/Foundation.h>
@interface RCTDevToolsRuntimeSettingsModule : NSObject
@end

View File

@@ -0,0 +1,58 @@
/*
* 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.
*/
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTDevToolsRuntimeSettingsModule.h>
#import "CoreModulesPlugins.h"
struct Config {
bool shouldReloadAndProfile = false;
bool recordChangeDescriptions = false;
};
// static to persist across Turbo Module reloads
static Config _config;
@interface RCTDevToolsRuntimeSettingsModule () <NativeReactDevToolsRuntimeSettingsModuleSpec> {
}
@end
@implementation RCTDevToolsRuntimeSettingsModule
RCT_EXPORT_MODULE(ReactDevToolsRuntimeSettingsModule)
RCT_EXPORT_METHOD(
setReloadAndProfileConfig : (JS::NativeReactDevToolsRuntimeSettingsModule::PartialReloadAndProfileConfig &)config)
{
if (config.shouldReloadAndProfile().has_value()) {
_config.shouldReloadAndProfile = config.shouldReloadAndProfile().value();
}
if (config.recordChangeDescriptions().has_value()) {
_config.recordChangeDescriptions = config.recordChangeDescriptions().value();
}
}
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getReloadAndProfileConfig)
{
return @{
@"shouldReloadAndProfile" : @(_config.shouldReloadAndProfile),
@"recordChangeDescriptions" : @(_config.recordChangeDescriptions),
};
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeReactDevToolsRuntimeSettingsModuleSpecJSI>(params);
}
@end
Class RCTDevToolsRuntimeSettingsCls(void)
{
return RCTDevToolsRuntimeSettingsModule.class;
}

View File

@@ -0,0 +1,16 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
@interface RCTDeviceInfo : NSObject <RCTBridgeModule>
- (instancetype)initWithDimensionsProvider:(NSDictionary * (^)(void))dimensionsProvider;
@end

View File

@@ -0,0 +1,325 @@
/*
* 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.
*/
#import "RCTDeviceInfo.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTAccessibilityManager.h>
#import <React/RCTAssert.h>
#import <React/RCTConstants.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTInitializing.h>
#import <React/RCTInvalidating.h>
#import <React/RCTUtils.h>
#import <atomic>
#import "CoreModulesPlugins.h"
using namespace facebook::react;
@interface RCTDeviceInfo () <NativeDeviceInfoSpec, RCTInitializing, RCTInvalidating>
@end
@implementation RCTDeviceInfo {
UIInterfaceOrientation _currentInterfaceOrientation;
NSDictionary *_currentInterfaceDimensions;
BOOL _isFullscreen;
std::atomic<BOOL> _invalidated;
NSDictionary *_constants;
__weak UIWindow *_applicationWindow;
NSDictionary * (^_dimensionsProvider)(void);
}
static NSString *const kFrameKeyPath = @"frame";
@synthesize moduleRegistry = _moduleRegistry;
RCT_EXPORT_MODULE()
- (instancetype)init
{
if (self = [super init]) {
_applicationWindow = RCTKeyWindow();
[_applicationWindow addObserver:self forKeyPath:kFrameKeyPath options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
- (instancetype)initWithDimensionsProvider:(NSDictionary * (^)(void))dimensionsProvider
{
if (self = [self init]) {
_dimensionsProvider = dimensionsProvider;
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:kFrameKeyPath]) {
[self interfaceFrameDidChange];
[[NSNotificationCenter defaultCenter] postNotificationName:RCTWindowFrameDidChangeNotification object:self];
}
}
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)initialize
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveNewContentSizeMultiplier)
name:RCTAccessibilityManagerDidUpdateMultiplierNotification
object:[_moduleRegistry moduleForName:"AccessibilityManager"]];
_currentInterfaceDimensions = [self _exportedDimensions];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(interfaceOrientationDidChange)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(interfaceFrameDidChange)
name:RCTUserInterfaceStyleDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(interfaceFrameDidChange)
name:UIApplicationDidBecomeActiveNotification
object:nil];
#if TARGET_OS_IOS
_currentInterfaceOrientation = RCTKeyWindow().windowScene.interfaceOrientation;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(interfaceFrameDidChange)
name:UIDeviceOrientationDidChangeNotification
object:nil];
#endif
// TODO T175901725 - Registering the RCTDeviceInfo module to the notification is a short-term fix to unblock 0.73
// The actual behavior should be that the module is properly registered in the TurboModule/Bridge infrastructure
// and the infrastructure imperatively invoke the `invalidate` method, rather than listening to a notification.
// This is a temporary workaround until we can investigate the issue better as there might be other modules in a
// similar situation.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(invalidate)
name:RCTBridgeWillInvalidateModulesNotification
object:nil];
_constants = @{
@"Dimensions" : [self _exportedDimensions],
// Note:
// This prop is deprecated and will be removed in a future release.
// Please use this only for a quick and temporary solution.
// Use <SafeAreaView> instead.
@"isIPhoneX_deprecated" : @(RCTIsIPhoneNotched()),
};
}
- (void)invalidate
{
if (_invalidated.exchange(YES)) {
return;
}
[self _cleanupObservers];
}
- (void)_cleanupObservers
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:RCTAccessibilityManagerDidUpdateMultiplierNotification
object:[_moduleRegistry moduleForName:"AccessibilityManager"]];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:RCTUserInterfaceStyleDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:RCTBridgeWillInvalidateModulesNotification object:nil];
[_applicationWindow removeObserver:self forKeyPath:kFrameKeyPath];
#if TARGET_OS_IOS
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
#endif
}
static BOOL RCTIsIPhoneNotched()
{
static BOOL isIPhoneNotched = NO;
static dispatch_once_t onceToken;
#if TARGET_OS_IOS
dispatch_once(&onceToken, ^{
RCTAssertMainQueue();
// 20pt is the top safeArea value in non-notched devices
UIWindow *keyWindow = RCTKeyWindow();
if (keyWindow) {
isIPhoneNotched = keyWindow.safeAreaInsets.top > 20;
}
});
#endif
return isIPhoneNotched;
}
static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
{
RCTAssertMainQueue();
UIScreen *mainScreen = UIScreen.mainScreen;
CGSize screenSize = mainScreen.bounds.size;
UIView *mainWindow = RCTKeyWindow();
// We fallback to screen size if a key window is not found.
CGSize windowSize = mainWindow ? mainWindow.bounds.size : screenSize;
NSDictionary<NSString *, NSNumber *> *dimsWindow = @{
@"width" : @(windowSize.width),
@"height" : @(windowSize.height),
@"scale" : @(mainScreen.scale),
@"fontScale" : @(fontScale)
};
NSDictionary<NSString *, NSNumber *> *dimsScreen = @{
@"width" : @(screenSize.width),
@"height" : @(screenSize.height),
@"scale" : @(mainScreen.scale),
@"fontScale" : @(fontScale)
};
return @{@"window" : dimsWindow, @"screen" : dimsScreen};
}
- (NSDictionary *)_exportedDimensions
{
// if a window size provider has been set, use that. If nil is returned from the provider
// it will fall back to the default behavior.
if (_dimensionsProvider != nil) {
auto dimensions = _dimensionsProvider();
if (dimensions != nil) {
return dimensions;
}
}
RCTAssert(!_invalidated, @"Failed to get exported dimensions: RCTDeviceInfo has been invalidated");
RCTAssert(_moduleRegistry, @"Failed to get exported dimensions: RCTModuleRegistry is nil");
RCTAccessibilityManager *accessibilityManager =
(RCTAccessibilityManager *)[_moduleRegistry moduleForName:"AccessibilityManager"];
// TOOD(T225745315): For some reason, accessibilityManager is nil in some cases.
// We default the fontScale to 1.0 in this case. This should be okay: if we assume
// that accessibilityManager will eventually become available, js will eventually
// be updated with the correct fontScale.
CGFloat fontScale = accessibilityManager ? accessibilityManager.multiplier : 1.0;
return RCTExportedDimensions(fontScale);
}
- (NSDictionary<NSString *, id> *)constantsToExport
{
return [self getConstants];
}
- (NSDictionary<NSString *, id> *)getConstants
{
return _constants;
}
- (void)didReceiveNewContentSizeMultiplier
{
__weak __typeof(self) weakSelf = self;
RCTModuleRegistry *moduleRegistry = _moduleRegistry;
RCTExecuteOnMainQueue(^{
// Report the event across the bridge.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"didUpdateDimensions"
body:[weakSelf _exportedDimensions]];
#pragma clang diagnostic pop
});
}
- (void)interfaceOrientationDidChange
{
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
UIWindow *window = RCTKeyWindow();
UIInterfaceOrientation nextOrientation = window.windowScene.interfaceOrientation;
BOOL isRunningInFullScreen = window ? CGRectEqualToRect(window.frame, window.screen.bounds) : YES;
// We are catching here two situations for multitasking view:
// a) The app is in Split View and the container gets resized -> !isRunningInFullScreen
// b) The app changes to/from fullscreen example: App runs in slide over mode and goes into fullscreen->
// isRunningInFullScreen != _isFullscreen The above two cases a || b can be shortened to !isRunningInFullScreen ||
// !_isFullscreen;
BOOL isResizingOrChangingToFullscreen = !isRunningInFullScreen || !_isFullscreen;
BOOL isOrientationChanging = (UIInterfaceOrientationIsPortrait(_currentInterfaceOrientation) &&
!UIInterfaceOrientationIsPortrait(nextOrientation)) ||
(UIInterfaceOrientationIsLandscape(_currentInterfaceOrientation) &&
!UIInterfaceOrientationIsLandscape(nextOrientation));
// Update when we go from portrait to landscape, or landscape to portrait
// Also update when the fullscreen state changes (multitasking) and only when the app is in active state.
if ((isOrientationChanging || isResizingOrChangingToFullscreen) && RCTIsAppActive()) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"didUpdateDimensions"
body:[self _exportedDimensions]];
// We only want to track the current _currentInterfaceOrientation and _isFullscreen only
// when it happens and only when it is published.
_currentInterfaceOrientation = nextOrientation;
_isFullscreen = isRunningInFullScreen;
#pragma clang diagnostic pop
}
#endif
}
- (void)interfaceFrameDidChange
{
__weak __typeof(self) weakSelf = self;
RCTExecuteOnMainQueue(^{
[weakSelf _interfaceFrameDidChange];
});
}
- (void)_interfaceFrameDidChange
{
NSDictionary *nextInterfaceDimensions = [self _exportedDimensions];
// update and publish the even only when the app is in active state
if (!([nextInterfaceDimensions isEqual:_currentInterfaceDimensions]) && RCTIsAppActive()) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"didUpdateDimensions"
body:nextInterfaceDimensions];
// We only want to track the current _currentInterfaceOrientation only
// when it happens and only when it is published.
_currentInterfaceDimensions = nextInterfaceDimensions;
#pragma clang diagnostic pop
}
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeDeviceInfoSpecJSI>(params);
}
@end
Class RCTDeviceInfoCls(void)
{
return RCTDeviceInfo.class;
}

View File

@@ -0,0 +1,18 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTInitializing.h>
/**
* This class wraps the -[RCTBridge enqueueJSCall:args:] method, and
* provides some convenience methods for generating event calls.
*/
@interface RCTEventDispatcher : NSObject <RCTEventDispatcherProtocol, RCTInitializing>
@end

View File

@@ -0,0 +1,257 @@
/*
* 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.
*/
#import "RCTEventDispatcher.h"
#import <React/RCTAssert.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTComponentEvent.h>
#import <React/RCTProfile.h>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTTurboModule.h>
#import "CoreModulesPlugins.h"
static NSNumber *RCTGetEventID(NSNumber *viewTag, NSString *eventName, uint16_t coalescingKey)
{
return @(viewTag.intValue | (((uint64_t)eventName.hash & 0xFFFF) << 32) | (((uint64_t)coalescingKey) << 48));
}
static uint16_t RCTUniqueCoalescingKeyGenerator = 0;
@interface RCTEventDispatcher () <RCTTurboModule>
@end
@implementation RCTEventDispatcher {
// We need this lock to protect access to _events, _eventQueue and _eventsDispatchScheduled. It's filled in on main
// thread and consumed on js thread.
NSLock *_eventQueueLock;
// We have this id -> event mapping so we coalesce effectively.
NSMutableDictionary<NSNumber *, id<RCTEvent>> *_events;
// This array contains ids of events in order they come in, so we can emit them to JS in the exact same order.
NSMutableArray<NSNumber *> *_eventQueue;
BOOL _eventsDispatchScheduled;
NSHashTable<id<RCTEventDispatcherObserver>> *_observers;
NSRecursiveLock *_observersLock;
}
@synthesize bridge = _bridge;
@synthesize dispatchToJSThread = _dispatchToJSThread;
@synthesize callableJSModules = _callableJSModules;
RCT_EXPORT_MODULE()
- (void)initialize
{
_events = [NSMutableDictionary new];
_eventQueue = [NSMutableArray new];
_eventQueueLock = [NSLock new];
_eventsDispatchScheduled = NO;
_observers = [NSHashTable weakObjectsHashTable];
_observersLock = [NSRecursiveLock new];
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(_notifyEventDispatcherObserversOfEvent_DEPRECATED:)
name:RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED
object:nil];
}
- (void)sendViewEventWithName:(NSString *)name reactTag:(NSNumber *)reactTag
{
[_callableJSModules invokeModule:@"RCTViewEventEmitter" method:@"emit" withArgs:@[ name, RCTNullIfNil(reactTag) ]];
}
- (void)sendAppEventWithName:(NSString *)name body:(id)body
{
[_callableJSModules invokeModule:@"RCTNativeAppEventEmitter"
method:@"emit"
withArgs:(body != nullptr) ? @[ name, body ] : @[ name ]];
}
- (void)sendDeviceEventWithName:(NSString *)name body:(id)body
{
[_callableJSModules invokeModule:@"RCTDeviceEventEmitter"
method:@"emit"
withArgs:(body != nullptr) ? @[ name, body ] : @[ name ]];
}
- (void)sendTextEventWithType:(RCTTextEventType)type
reactTag:(NSNumber *)reactTag
text:(NSString *)text
key:(NSString *)key
eventCount:(NSInteger)eventCount
{
static NSString *events[] = {@"focus", @"blur", @"change", @"submitEditing", @"endEditing", @"keyPress"};
NSMutableDictionary *body = [[NSMutableDictionary alloc] initWithDictionary:@{
@"eventCount" : @(eventCount),
}];
if (text != nullptr) {
// We copy the string here because if it's a mutable string it may get released before we dispatch the event on a
// different thread, causing a crash.
body[@"text"] = [text copy];
}
if (key != nullptr) {
if (key.length == 0) {
key = @"Backspace"; // backspace
} else {
switch ([key characterAtIndex:0]) {
case '\t':
key = @"Tab";
break;
case '\n':
key = @"Enter";
break;
default:
break;
}
}
// We copy the string here because if it's a mutable string it may get released before we dispatch the event on a
// different thread, causing a crash.
body[@"key"] = [key copy];
}
RCTComponentEvent *event = [[RCTComponentEvent alloc] initWithName:events[type] viewTag:reactTag body:body];
[self sendEvent:event];
}
- (void)notifyObserversOfEvent:(id<RCTEvent>)event
{
[_observersLock lock];
for (id<RCTEventDispatcherObserver> observer in _observers) {
[observer eventDispatcherWillDispatchEvent:event];
}
[_observersLock unlock];
}
- (void)sendEvent:(id<RCTEvent>)event
{
[self notifyObserversOfEvent:event];
[_eventQueueLock lock];
NSNumber *eventID;
if (event.canCoalesce) {
eventID = RCTGetEventID(event.viewTag, event.eventName, event.coalescingKey);
id<RCTEvent> previousEvent = _events[eventID];
if (previousEvent != nullptr) {
event = [previousEvent coalesceWithEvent:event];
} else {
[_eventQueue addObject:eventID];
}
} else {
id<RCTEvent> previousEvent = _events[eventID];
eventID = RCTGetEventID(event.viewTag, event.eventName, RCTUniqueCoalescingKeyGenerator++);
RCTAssert(
previousEvent == nil,
@"Got event %@ which cannot be coalesced, but has the same eventID %@ as the previous event %@",
event,
eventID,
previousEvent);
[_eventQueue addObject:eventID];
}
_events[eventID] = event;
BOOL scheduleEventsDispatch = NO;
if (!_eventsDispatchScheduled) {
_eventsDispatchScheduled = YES;
scheduleEventsDispatch = YES;
}
// We have to release the lock before dispatching block with events,
// since dispatchBlock: can be executed synchronously on the same queue.
// (This is happening when chrome debugging is turned on.)
[_eventQueueLock unlock];
if (scheduleEventsDispatch) {
if (_bridge != nullptr) {
[_bridge
dispatchBlock:^{
[self flushEventsQueue];
}
queue:RCTJSThread];
} else if (_dispatchToJSThread != nullptr) {
_dispatchToJSThread(^{
[self flushEventsQueue];
});
}
}
}
- (void)addDispatchObserver:(id<RCTEventDispatcherObserver>)observer
{
[_observersLock lock];
[_observers addObject:observer];
[_observersLock unlock];
}
- (void)removeDispatchObserver:(id<RCTEventDispatcherObserver>)observer
{
[_observersLock lock];
[_observers removeObject:observer];
[_observersLock unlock];
}
- (void)dispatchEvent:(id<RCTEvent>)event
{
NSString *moduleDotMethod = [[event class] moduleDotMethod];
NSArray<NSString *> *const components = [moduleDotMethod componentsSeparatedByString:@"."];
NSString *const moduleName = components[0];
NSString *const methodName = components[1];
[_callableJSModules invokeModule:moduleName method:methodName withArgs:[event arguments]];
}
- (dispatch_queue_t)methodQueue
{
return RCTJSThread;
}
// js thread only (which surprisingly can be the main thread, depends on used JS executor)
- (void)flushEventsQueue
{
[_eventQueueLock lock];
NSDictionary *events = _events;
_events = [NSMutableDictionary new];
NSMutableArray *eventQueue = _eventQueue;
_eventQueue = [NSMutableArray new];
_eventsDispatchScheduled = NO;
[_eventQueueLock unlock];
for (NSNumber *eventId in eventQueue) {
[self dispatchEvent:events[eventId]];
}
}
- (void)_notifyEventDispatcherObserversOfEvent_DEPRECATED:(NSNotification *)notification
{
NSDictionary *userInfo = notification.userInfo;
id<RCTEvent> event = [userInfo objectForKey:@"event"];
if (event != nullptr) {
[self notifyObserversOfEvent:event];
}
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
@end
Class RCTEventDispatcherCls(void)
{
return RCTEventDispatcher.class;
}

View File

@@ -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.
*/
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
NS_ASSUME_NONNULL_BEGIN
@protocol RCTExceptionsManagerDelegate <NSObject>
- (void)handleSoftJSExceptionWithMessage:(nullable NSString *)message
stack:(nullable NSArray *)stack
exceptionId:(NSNumber *)exceptionId
extraDataAsJSON:(nullable NSString *)extraDataAsJSON;
- (void)handleFatalJSExceptionWithMessage:(nullable NSString *)message
stack:(nullable NSArray *)stack
exceptionId:(NSNumber *)exceptionId
extraDataAsJSON:(nullable NSString *)extraDataAsJSON;
@optional
- (NSDictionary<NSString *, id> *)decorateJSExceptionData:(NSDictionary<NSString *, id> *)exceptionData;
@end
@interface RCTExceptionsManager : NSObject <RCTBridgeModule>
- (instancetype)initWithDelegate:(id<RCTExceptionsManagerDelegate>)delegate;
- (void)reportSoftException:(nullable NSString *)message
stack:(nullable NSArray<NSDictionary *> *)stack
exceptionId:(double)exceptionId;
- (void)reportFatalException:(nullable NSString *)message
stack:(nullable NSArray<NSDictionary *> *)stack
exceptionId:(double)exceptionId;
- (void)reportJsException:(nullable NSString *)message
stack:(nullable NSArray<NSDictionary *> *)stack
exceptionId:(double)exceptionId
isFatal:(bool)isFatal __attribute__((deprecated));
@property (nonatomic, weak) id<RCTExceptionsManagerDelegate> delegate;
@property (nonatomic, assign) NSUInteger maxReloadAttempts;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,186 @@
/*
* 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.
*/
#import "RCTExceptionsManager.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConvert.h>
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
#import <React/RCTRedBox.h>
#import <React/RCTRedBoxSetEnabled.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTRootView.h>
#import "CoreModulesPlugins.h"
@interface RCTExceptionsManager () <NativeExceptionsManagerSpec>
@end
@implementation RCTExceptionsManager
@synthesize moduleRegistry = _moduleRegistry;
RCT_EXPORT_MODULE()
- (instancetype)initWithDelegate:(id<RCTExceptionsManagerDelegate>)delegate
{
if ((self = [self init]) != nullptr) {
_delegate = delegate;
}
return self;
}
- (void)reportSoft:(NSString *)message
stack:(NSArray<NSDictionary *> *)stack
exceptionId:(double)exceptionId
extraDataAsJSON:(nullable NSString *)extraDataAsJSON
{
if (RCTRedBoxGetEnabled()) {
RCTRedBox *redbox = [_moduleRegistry moduleForName:"RedBox"];
[redbox showErrorMessage:message withStack:stack errorCookie:(int)exceptionId];
}
if (_delegate != nullptr) {
[_delegate handleSoftJSExceptionWithMessage:message
stack:stack
exceptionId:[NSNumber numberWithDouble:exceptionId]
extraDataAsJSON:extraDataAsJSON];
}
}
- (void)reportFatal:(NSString *)message
stack:(NSArray<NSDictionary *> *)stack
exceptionId:(double)exceptionId
extraDataAsJSON:(nullable NSString *)extraDataAsJSON
{
if (RCTRedBoxGetEnabled()) {
RCTRedBox *redbox = [_moduleRegistry moduleForName:"RedBox"];
[redbox showErrorMessage:message withStack:stack errorCookie:(int)exceptionId];
}
if (_delegate != nullptr) {
[_delegate handleFatalJSExceptionWithMessage:message
stack:stack
exceptionId:[NSNumber numberWithDouble:exceptionId]
extraDataAsJSON:extraDataAsJSON];
}
static NSUInteger reloadRetries = 0;
if (!RCT_DEBUG && reloadRetries < _maxReloadAttempts) {
reloadRetries++;
RCTTriggerReloadCommandListeners(@"JS Crash Reload");
} else if (!RCT_DEV) {
NSString *description = [@"Unhandled JS Exception: " stringByAppendingString:message];
NSDictionary *errorInfo =
@{NSLocalizedDescriptionKey : description, RCTJSStackTraceKey : stack, RCTJSExtraDataKey : extraDataAsJSON};
RCTFatal([NSError errorWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]);
}
}
// TODO(T205456329): This method is deprecated in favour of reportException. Delete in v0.77
RCT_EXPORT_METHOD(
reportSoftException : (NSString *)message stack : (NSArray<NSDictionary *> *)stack exceptionId : (double)
exceptionId)
{
[self reportSoft:message stack:stack exceptionId:exceptionId extraDataAsJSON:nil];
}
// TODO(T205456329): This method is deprecated in favour of reportException. Delete in v0.77
RCT_EXPORT_METHOD(
reportFatalException : (NSString *)message stack : (NSArray<NSDictionary *> *)stack exceptionId : (double)
exceptionId)
{
[self reportFatal:message stack:stack exceptionId:exceptionId extraDataAsJSON:nil];
}
RCT_EXPORT_METHOD(dismissRedbox) {}
RCT_EXPORT_METHOD(reportException : (JS::NativeExceptionsManager::ExceptionData &)data)
{
NSMutableDictionary<NSString *, id> *mutableErrorData = [NSMutableDictionary new];
mutableErrorData[@"message"] = data.message();
if (data.originalMessage() != nullptr) {
mutableErrorData[@"originalMessage"] = data.originalMessage();
}
if (data.name() != nullptr) {
mutableErrorData[@"name"] = data.name();
}
if (data.componentStack() != nullptr) {
mutableErrorData[@"componentStack"] = data.componentStack();
}
// Reserialize data.stack() into an array of untyped dictionaries.
// TODO: (moti) T53588496 Replace `(NSArray<NSDictionary *> *)stack` in
// reportFatalException etc with a typed interface.
NSMutableArray<NSDictionary<NSString *, id> *> *stackArray = [NSMutableArray<NSDictionary<NSString *, id> *> new];
for (auto frame : data.stack()) {
NSMutableDictionary<NSString *, id> *frameDict = [NSMutableDictionary new];
if (frame.column().has_value()) {
frameDict[@"column"] = @(frame.column().value());
}
frameDict[@"file"] = frame.file();
if (frame.lineNumber().has_value()) {
frameDict[@"lineNumber"] = @(frame.lineNumber().value());
}
frameDict[@"methodName"] = frame.methodName();
if (frame.collapse().has_value()) {
frameDict[@"collapse"] = @(frame.collapse().value());
}
[stackArray addObject:frameDict];
}
mutableErrorData[@"stack"] = stackArray;
mutableErrorData[@"id"] = @(data.id_());
mutableErrorData[@"isFatal"] = @(data.isFatal());
if (data.extraData() != nullptr) {
mutableErrorData[@"extraData"] = data.extraData();
}
NSDictionary<NSString *, id> *errorData = mutableErrorData;
if ([_delegate respondsToSelector:@selector(decorateJSExceptionData:)]) {
errorData = [_delegate decorateJSExceptionData:errorData];
}
NSString *extraDataAsJSON = RCTJSONStringify(errorData[@"extraData"], NULL);
NSString *message = errorData[@"message"];
NSArray<NSDictionary<NSString *, id> *> *stack = errorData[@"stack"];
double exceptionId = [errorData[@"id"] doubleValue];
if ([errorData[@"isFatal"] boolValue]) {
[self reportFatal:message stack:stack exceptionId:exceptionId extraDataAsJSON:extraDataAsJSON];
} else {
[self reportSoft:message stack:stack exceptionId:exceptionId extraDataAsJSON:extraDataAsJSON];
}
}
- (void)reportJsException:(nullable NSString *)message
stack:(nullable NSArray<NSDictionary *> *)stack
exceptionId:(double)exceptionId
isFatal:(bool)isFatal
{
if (isFatal) {
[self reportFatalException:message stack:stack exceptionId:exceptionId];
} else {
[self reportSoftException:message stack:stack exceptionId:exceptionId];
}
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeExceptionsManagerSpecJSI>(params);
}
@end
Class RCTExceptionsManagerCls(void)
{
return RCTExceptionsManager.class;
}

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTDefines.h>
#if RCT_DEV
@interface RCTFPSGraph : UIView
@property (nonatomic, assign, readonly) NSUInteger FPS;
@property (nonatomic, assign, readonly) NSUInteger maxFPS;
@property (nonatomic, assign, readonly) NSUInteger minFPS;
- (instancetype)initWithFrame:(CGRect)frame color:(UIColor *)color NS_DESIGNATED_INITIALIZER;
- (void)onTick:(NSTimeInterval)timestamp;
@end
#endif

View File

@@ -0,0 +1,128 @@
/*
* 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.
*/
#import <React/RCTFPSGraph.h>
#import <React/RCTAssert.h>
#if RCT_DEV
@interface RCTFPSGraph ()
@property (nonatomic, strong, readonly) CAShapeLayer *graph;
@property (nonatomic, strong, readonly) UILabel *label;
@end
@implementation RCTFPSGraph {
CAShapeLayer *_graph;
UILabel *_label;
CGFloat *_frames;
UIColor *_color;
NSTimeInterval _prevTime;
NSUInteger _frameCount;
NSUInteger _FPS;
NSUInteger _maxFPS;
NSUInteger _minFPS;
NSUInteger _length;
NSUInteger _height;
CGFloat _scale;
}
- (instancetype)initWithFrame:(CGRect)frame color:(UIColor *)color
{
if ((self = [super initWithFrame:frame]) != nullptr) {
_frameCount = -1;
_prevTime = -1;
_maxFPS = 0;
_minFPS = 60;
_length = (NSUInteger)floor(frame.size.width);
_height = (NSUInteger)floor(frame.size.height);
_scale = 60.0 / (CGFloat)_height;
_frames = (CGFloat *)calloc(sizeof(CGFloat), _length);
_color = color;
[self.layer addSublayer:self.graph];
[self addSubview:self.label];
}
return self;
}
- (void)dealloc
{
free(_frames);
}
RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
- (CAShapeLayer *)graph
{
if (_graph == nullptr) {
_graph = [CAShapeLayer new];
_graph.frame = self.bounds;
_graph.backgroundColor = [_color colorWithAlphaComponent:0.2].CGColor;
_graph.fillColor = _color.CGColor;
}
return _graph;
}
- (UILabel *)label
{
if (_label == nullptr) {
_label = [[UILabel alloc] initWithFrame:self.bounds];
_label.font = [UIFont boldSystemFontOfSize:13];
_label.textAlignment = NSTextAlignmentCenter;
}
return _label;
}
- (void)onTick:(NSTimeInterval)timestamp
{
_frameCount++;
if (_prevTime == -1) {
_prevTime = timestamp;
} else if (timestamp - _prevTime >= 1) {
_FPS = round((double)_frameCount / (timestamp - _prevTime));
_minFPS = MIN(_minFPS, _FPS);
_maxFPS = MAX(_maxFPS, _FPS);
dispatch_async(dispatch_get_main_queue(), ^{
self->_label.text = [NSString stringWithFormat:@"%lu", (unsigned long)self->_FPS];
});
CGFloat previousScale = _scale;
CGFloat targetFps = MAX(_maxFPS, 60.0);
_scale = targetFps / (CGFloat)_height;
for (NSUInteger i = 0; i < _length - 1; i++) {
// Move each Frame back one position and adjust to new scale (if there is a new scale)
_frames[i] = _frames[i + 1] * previousScale / _scale;
}
_frames[_length - 1] = (double)_FPS / _scale;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, (CGFloat)_height);
for (NSUInteger i = 0; i < _length; i++) {
CGPathAddLineToPoint(path, NULL, (CGFloat)i, (double)_height - _frames[i]);
}
CGPathAddLineToPoint(path, NULL, (CGFloat)_length - 1, (CGFloat)_height);
_graph.path = path;
CGPathRelease(path);
_prevTime = timestamp;
_frameCount = 0;
}
}
@end
#endif

View File

@@ -0,0 +1,17 @@
/*
* 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.
*/
#import <React/RCTBridgeModule.h>
/**
* @experimental
* This is a experimental module for RTL support
* This module bridges the i18n utility from RCTI18nUtil
*/
@interface RCTI18nManager : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,67 @@
/*
* 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.
*/
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTI18nUtil.h>
#import "RCTI18nManager.h"
#import "CoreModulesPlugins.h"
using namespace facebook::react;
@interface RCTI18nManager () <NativeI18nManagerSpec>
@end
@implementation RCTI18nManager
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
RCT_EXPORT_METHOD(allowRTL : (BOOL)value)
{
[[RCTI18nUtil sharedInstance] allowRTL:value];
}
RCT_EXPORT_METHOD(forceRTL : (BOOL)value)
{
[[RCTI18nUtil sharedInstance] forceRTL:value];
}
RCT_EXPORT_METHOD(swapLeftAndRightInRTL : (BOOL)value)
{
[[RCTI18nUtil sharedInstance] swapLeftAndRightInRTL:value];
}
- (NSDictionary *)constantsToExport
{
return [self getConstants];
}
- (NSDictionary *)getConstants
{
return @{
@"isRTL" : @([[RCTI18nUtil sharedInstance] isRTL]),
@"doLeftAndRightSwapInRTL" : @([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL])
};
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeI18nManagerSpecJSI>(params);
}
@end
Class RCTI18nManagerCls(void)
{
return RCTI18nManager.class;
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
#import <React/RCTEventEmitter.h>
@interface RCTKeyboardObserver : RCTEventEmitter
@end

View File

@@ -0,0 +1,137 @@
/*
* 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.
*/
#import "RCTKeyboardObserver.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification);
@interface RCTKeyboardObserver () <NativeKeyboardObserverSpec>
@end
@implementation RCTKeyboardObserver
RCT_EXPORT_MODULE()
- (void)startObserving
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
#define ADD_KEYBOARD_HANDLER(NAME, SELECTOR) [nc addObserver:self selector:@selector(SELECTOR:) name:NAME object:nil]
ADD_KEYBOARD_HANDLER(UIKeyboardWillShowNotification, keyboardWillShow);
ADD_KEYBOARD_HANDLER(UIKeyboardDidShowNotification, keyboardDidShow);
ADD_KEYBOARD_HANDLER(UIKeyboardWillHideNotification, keyboardWillHide);
ADD_KEYBOARD_HANDLER(UIKeyboardDidHideNotification, keyboardDidHide);
ADD_KEYBOARD_HANDLER(UIKeyboardWillChangeFrameNotification, keyboardWillChangeFrame);
ADD_KEYBOARD_HANDLER(UIKeyboardDidChangeFrameNotification, keyboardDidChangeFrame);
#undef ADD_KEYBOARD_HANDLER
}
- (NSArray<NSString *> *)supportedEvents
{
return @[
@"keyboardWillShow",
@"keyboardDidShow",
@"keyboardWillHide",
@"keyboardDidHide",
@"keyboardWillChangeFrame",
@"keyboardDidChangeFrame"
];
}
- (void)stopObserving
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// Bridge might be already invalidated by the time the keyboard is about to be dismissed.
// This might happen, for example, when reload from the packager is performed.
// Thus we need to check against nil here.
#define IMPLEMENT_KEYBOARD_HANDLER(EVENT) \
-(void)EVENT : (NSNotification *)notification \
{ \
if (!self.callableJSModules) { \
return; \
} \
[self sendEventWithName:@ #EVENT body:RCTParseKeyboardNotification(notification)]; \
}
IMPLEMENT_KEYBOARD_HANDLER(keyboardWillShow)
IMPLEMENT_KEYBOARD_HANDLER(keyboardDidShow)
IMPLEMENT_KEYBOARD_HANDLER(keyboardWillHide)
IMPLEMENT_KEYBOARD_HANDLER(keyboardDidHide)
IMPLEMENT_KEYBOARD_HANDLER(keyboardWillChangeFrame)
IMPLEMENT_KEYBOARD_HANDLER(keyboardDidChangeFrame)
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeKeyboardObserverSpecJSI>(params);
}
@end
NS_INLINE NSDictionary *RCTRectDictionaryValue(CGRect rect)
{
return @{
@"screenX" : @(rect.origin.x),
@"screenY" : @(rect.origin.y),
@"width" : @(rect.size.width),
@"height" : @(rect.size.height),
};
}
static NSString *RCTAnimationNameForCurve(UIViewAnimationCurve curve)
{
switch (curve) {
case UIViewAnimationCurveEaseIn:
return @"easeIn";
case UIViewAnimationCurveEaseInOut:
return @"easeInEaseOut";
case UIViewAnimationCurveEaseOut:
return @"easeOut";
case UIViewAnimationCurveLinear:
return @"linear";
default:
return @"keyboard";
}
}
static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification)
{
NSDictionary *userInfo = notification.userInfo;
CGRect beginFrame = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve curve =
static_cast<UIViewAnimationCurve>([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]);
NSInteger isLocalUserInfoKey = [userInfo[UIKeyboardIsLocalUserInfoKey] integerValue];
UIWindow *window = RCTKeyWindow();
beginFrame = [window convertRect:beginFrame fromCoordinateSpace:window.screen.coordinateSpace];
endFrame = [window convertRect:endFrame fromCoordinateSpace:window.screen.coordinateSpace];
return @{
@"startCoordinates" : RCTRectDictionaryValue(beginFrame),
@"endCoordinates" : RCTRectDictionaryValue(endFrame),
@"duration" : @(duration * 1000.0), // ms
@"easing" : RCTAnimationNameForCurve(curve),
@"isEventFromThisApp" : isLocalUserInfoKey == 1 ? @YES : @NO,
};
}
Class RCTKeyboardObserverCls(void)
{
return RCTKeyboardObserver.class;
}

View File

@@ -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.
*/
#import <UIKit/UIKit.h>
#import "RCTLogBoxView.h"
@interface RCTLogBox : NSObject
#if RCT_DEV_MENU
- (void)setRCTLogBoxView:(RCTLogBoxView *)view;
#endif
@end

View File

@@ -0,0 +1,139 @@
/*
* 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.
*/
#import "RCTLogBox.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTBridge.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>
#import <React/RCTRedBoxSetEnabled.h>
#import <React/RCTSurface.h>
#import "CoreModulesPlugins.h"
#if RCT_DEV_MENU
@interface RCTLogBox () <NativeLogBoxSpec, RCTBridgeModule>
@end
@implementation RCTLogBox {
RCTLogBoxView *_view;
__weak id<RCTSurfacePresenterStub> _bridgelessSurfacePresenter;
}
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (void)setSurfacePresenter:(id<RCTSurfacePresenterStub>)surfacePresenter
{
_bridgelessSurfacePresenter = surfacePresenter;
}
RCT_EXPORT_METHOD(show)
{
if (RCTRedBoxGetEnabled()) {
__weak RCTLogBox *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong RCTLogBox *strongSelf = weakSelf;
if (strongSelf == nullptr) {
return;
}
if (strongSelf->_view != nullptr) {
[strongSelf->_view show];
return;
}
if (strongSelf->_bridgelessSurfacePresenter != nullptr) {
strongSelf->_view = [[RCTLogBoxView alloc] initWithWindow:RCTKeyWindow()
surfacePresenter:strongSelf->_bridgelessSurfacePresenter];
[strongSelf->_view show];
}
#ifndef RCT_REMOVE_LEGACY_ARCH
else if ((strongSelf->_bridge != nullptr) && strongSelf->_bridge.valid) {
if (strongSelf->_bridge.surfacePresenter != nullptr) {
strongSelf->_view = [[RCTLogBoxView alloc] initWithWindow:RCTKeyWindow()
surfacePresenter:strongSelf->_bridge.surfacePresenter];
} else {
strongSelf->_view = [[RCTLogBoxView alloc] initWithWindow:RCTKeyWindow() bridge:strongSelf->_bridge];
}
[strongSelf->_view show];
}
#endif // RCT_REMOVE_LEGACY_ARCH
});
}
}
RCT_EXPORT_METHOD(hide)
{
if (RCTRedBoxGetEnabled()) {
__weak RCTLogBox *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong RCTLogBox *strongSelf = weakSelf;
if (strongSelf == nullptr) {
return;
}
[strongSelf->_view setHidden:YES];
strongSelf->_view = nil;
});
}
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeLogBoxSpecJSI>(params);
}
- (void)setRCTLogBoxView:(RCTLogBoxView *)view
{
self->_view = view;
}
@end
#else // Disabled
@interface RCTLogBox () <NativeLogBoxSpec>
@end
@implementation RCTLogBox
+ (NSString *)moduleName
{
return nil;
}
- (void)show
{
// noop
}
- (void)hide
{
// noop
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeLogBoxSpecJSI>(params);
}
@end
#endif
Class RCTLogBoxCls(void)
{
return RCTLogBox.class;
}

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
#import <React/RCTBridge.h>
#import <React/RCTSurfacePresenterStub.h>
#import <React/RCTSurfaceView.h>
#import <UIKit/UIKit.h>
@interface RCTLogBoxView : UIWindow
- (instancetype)initWithFrame:(CGRect)frame;
- (void)createRootViewController:(UIView *)view;
#ifndef RCT_REMOVE_LEGACY_ARCH
- (instancetype)initWithWindow:(UIWindow *)window bridge:(RCTBridge *)bridge;
#endif // RCT_REMOVE_LEGACY_ARCH
- (instancetype)initWithWindow:(UIWindow *)window surfacePresenter:(id<RCTSurfacePresenterStub>)surfacePresenter;
- (void)show;
@end

View File

@@ -0,0 +1,93 @@
/*
* 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.
*/
#import "RCTLogBoxView.h"
#import <React/RCTLog.h>
#import <React/RCTSurface.h>
#import <React/RCTSurfaceHostingView.h>
@implementation RCTLogBoxView {
#ifndef RCT_REMOVE_LEGACY_ARCH
RCTSurface *_surface;
#endif // RCT_REMOVE_LEGACY_ARCH
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]) != nullptr) {
self.windowLevel = UIWindowLevelStatusBar - 1;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)createRootViewController:(UIView *)view
{
UIViewController *_rootViewController = [UIViewController new];
_rootViewController.view = view;
_rootViewController.view.backgroundColor = [UIColor clearColor];
_rootViewController.modalPresentationStyle = UIModalPresentationFullScreen;
self.rootViewController = _rootViewController;
}
#ifndef RCT_REMOVE_LEGACY_ARCH
- (instancetype)initWithWindow:(UIWindow *)window bridge:(RCTBridge *)bridge
{
self = [super initWithWindowScene:window.windowScene];
self.windowLevel = UIWindowLevelStatusBar - 1;
self.backgroundColor = [UIColor clearColor];
_surface = [[RCTSurface alloc] initWithBridge:bridge moduleName:@"LogBox" initialProperties:@{}];
[_surface start];
if (![_surface synchronouslyWaitForStage:RCTSurfaceStageSurfaceDidInitialMounting timeout:1]) {
RCTLogInfo(@"Failed to mount LogBox within 1s");
}
[self createRootViewController:(UIView *)_surface.view];
return self;
}
#endif // RCT_REMOVE_LEGACY_ARCH
- (instancetype)initWithWindow:(UIWindow *)window surfacePresenter:(id<RCTSurfacePresenterStub>)surfacePresenter
{
self = [super initWithWindowScene:window.windowScene];
id<RCTSurfaceProtocol> surface = [surfacePresenter createFabricSurfaceForModuleName:@"LogBox" initialProperties:@{}];
[surface start];
RCTSurfaceHostingView *rootView = [[RCTSurfaceHostingView alloc]
initWithSurface:surface
sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact];
[self createRootViewController:rootView];
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
#ifndef RCT_REMOVE_LEGACY_ARCH
[_surface setSize:self.frame.size];
#endif // RCT_REMOVE_LEGACY_ARCH
}
- (void)dealloc
{
#if !TARGET_OS_MACCATALYST // sharedApplication.delegate is not available on Mac Catalyst
[RCTSharedApplication().delegate.window makeKeyWindow];
#endif
}
- (void)show
{
[self becomeFirstResponder];
[self makeKeyAndVisible];
}
@end

View File

@@ -0,0 +1,559 @@
/*
* 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.
*/
#import <React/RCTDefines.h>
#import "CoreModulesPlugins.h"
#if RCT_DEV
#import <dlfcn.h>
#import <mach/mach.h>
#import <React/RCTDevSettings.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTFPSGraph.h>
#import <React/RCTInitializing.h>
#import <React/RCTInvalidating.h>
#import <React/RCTJavaScriptExecutor.h>
#import <React/RCTPerformanceLogger.h>
#import <React/RCTPerformanceLoggerLabels.h>
#import <React/RCTRootView.h>
#import <React/RCTUIManager.h>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTTurboModule.h>
#if __has_include(<React/RCTDevMenu.h>)
#import <React/RCTDevMenu.h>
#endif
static NSString *const RCTPerfMonitorCellIdentifier = @"RCTPerfMonitorCellIdentifier";
static const CGFloat RCTPerfMonitorBarHeight = 50;
static const CGFloat RCTPerfMonitorExpandHeight = 250;
using RCTJSCSetOptionType = BOOL (*)(const char *);
NSArray<NSString *> *LabelsForRCTPerformanceLoggerTags();
static BOOL RCTJSCSetOption(const char *option)
{
return NO;
}
static vm_size_t RCTGetResidentMemorySize(void)
{
vm_size_t memoryUsageInByte = 0;
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
if (kernelReturn == KERN_SUCCESS) {
memoryUsageInByte = (vm_size_t)vmInfo.phys_footprint;
}
return memoryUsageInByte;
}
@interface RCTPerfMonitor
: NSObject <RCTBridgeModule, RCTTurboModule, RCTInvalidating, UITableViewDataSource, UITableViewDelegate>
#if __has_include(<React/RCTDevMenu.h>)
@property (nonatomic, strong, readonly) RCTDevMenuItem *devMenuItem;
#endif
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *gestureRecognizer;
@property (nonatomic, strong, readonly) UIView *container;
@property (nonatomic, strong, readonly) UILabel *memory;
@property (nonatomic, strong, readonly) UILabel *heap;
@property (nonatomic, strong, readonly) UILabel *views;
@property (nonatomic, strong, readonly) UITableView *metrics;
@property (nonatomic, strong, readonly) RCTFPSGraph *jsGraph;
@property (nonatomic, strong, readonly) RCTFPSGraph *uiGraph;
@property (nonatomic, strong, readonly) UILabel *jsGraphLabel;
@property (nonatomic, strong, readonly) UILabel *uiGraphLabel;
@end
@implementation RCTPerfMonitor {
#if __has_include(<React/RCTDevMenu.h>)
RCTDevMenuItem *_devMenuItem;
#endif
UIPanGestureRecognizer *_gestureRecognizer;
UIView *_container;
UILabel *_memory;
UILabel *_heap;
UILabel *_views;
UILabel *_uiGraphLabel;
UILabel *_jsGraphLabel;
UITableView *_metrics;
RCTFPSGraph *_uiGraph;
RCTFPSGraph *_jsGraph;
CADisplayLink *_uiDisplayLink;
CADisplayLink *_jsDisplayLink;
NSUInteger _heapSize;
dispatch_queue_t _queue;
dispatch_io_t _io;
int _stderr;
int _pipe[2];
NSString *_remaining;
CGRect _storedMonitorFrame;
NSArray *_perfLoggerMarks;
}
@synthesize bridge = _bridge;
@synthesize moduleRegistry = _moduleRegistry;
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
[self hide];
}
#if __has_include(<React/RCTDevMenu.h>)
- (RCTDevMenuItem *)devMenuItem
{
if (_devMenuItem == nullptr) {
__weak __typeof__(self) weakSelf = self;
__weak RCTDevSettings *devSettings = [self->_moduleRegistry moduleForName:"DevSettings"];
if (devSettings.isPerfMonitorShown) {
[weakSelf show];
}
_devMenuItem = [RCTDevMenuItem
buttonItemWithTitleBlock:^NSString * {
return (devSettings.isPerfMonitorShown) ? @"Hide Perf Monitor" : @"Show Perf Monitor";
}
handler:^{
if (devSettings.isPerfMonitorShown) {
[weakSelf hide];
devSettings.isPerfMonitorShown = NO;
} else {
[weakSelf show];
devSettings.isPerfMonitorShown = YES;
}
}];
}
return _devMenuItem;
}
#endif
- (UIPanGestureRecognizer *)gestureRecognizer
{
if (_gestureRecognizer == nullptr) {
_gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gesture:)];
}
return _gestureRecognizer;
}
- (UIView *)container
{
if (_container == nullptr) {
UIEdgeInsets safeInsets = RCTKeyWindow().safeAreaInsets;
_container =
[[UIView alloc] initWithFrame:CGRectMake(safeInsets.left, safeInsets.top, 180, RCTPerfMonitorBarHeight)];
_container.layer.borderWidth = 2;
_container.layer.borderColor = [UIColor lightGrayColor].CGColor;
[_container addGestureRecognizer:self.gestureRecognizer];
[_container addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)]];
_container.backgroundColor = [UIColor systemBackgroundColor];
}
return _container;
}
- (UILabel *)memory
{
if (_memory == nullptr) {
_memory = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 44, RCTPerfMonitorBarHeight)];
_memory.font = [UIFont systemFontOfSize:12];
_memory.numberOfLines = 3;
_memory.textAlignment = NSTextAlignmentCenter;
}
return _memory;
}
- (UILabel *)heap
{
if (_heap == nullptr) {
_heap = [[UILabel alloc] initWithFrame:CGRectMake(44, 0, 44, RCTPerfMonitorBarHeight)];
_heap.font = [UIFont systemFontOfSize:12];
_heap.numberOfLines = 3;
_heap.textAlignment = NSTextAlignmentCenter;
}
return _heap;
}
- (UILabel *)views
{
if (_views == nullptr) {
_views = [[UILabel alloc] initWithFrame:CGRectMake(88, 0, 44, RCTPerfMonitorBarHeight)];
_views.font = [UIFont systemFontOfSize:12];
_views.numberOfLines = 3;
_views.textAlignment = NSTextAlignmentCenter;
}
return _views;
}
- (RCTFPSGraph *)uiGraph
{
if (_uiGraph == nullptr) {
_uiGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(134, 14, 40, 30) color:[UIColor lightGrayColor]];
}
return _uiGraph;
}
- (RCTFPSGraph *)jsGraph
{
if (_jsGraph == nullptr) {
_jsGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(178, 14, 40, 30) color:[UIColor lightGrayColor]];
}
return _jsGraph;
}
- (UILabel *)uiGraphLabel
{
if (_uiGraphLabel == nullptr) {
_uiGraphLabel = [[UILabel alloc] initWithFrame:CGRectMake(134, 3, 40, 10)];
_uiGraphLabel.font = [UIFont systemFontOfSize:11];
_uiGraphLabel.textAlignment = NSTextAlignmentCenter;
_uiGraphLabel.text = @"UI";
}
return _uiGraphLabel;
}
- (UILabel *)jsGraphLabel
{
if (_jsGraphLabel == nullptr) {
_jsGraphLabel = [[UILabel alloc] initWithFrame:CGRectMake(178, 3, 38, 10)];
_jsGraphLabel.font = [UIFont systemFontOfSize:11];
_jsGraphLabel.textAlignment = NSTextAlignmentCenter;
_jsGraphLabel.text = @"JS";
}
return _jsGraphLabel;
}
- (UITableView *)metrics
{
if (_metrics == nullptr) {
_metrics = [[UITableView alloc] initWithFrame:CGRectMake(
0,
RCTPerfMonitorBarHeight,
self.container.frame.size.width,
self.container.frame.size.height - RCTPerfMonitorBarHeight)];
_metrics.dataSource = self;
_metrics.delegate = self;
_metrics.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[_metrics registerClass:[UITableViewCell class] forCellReuseIdentifier:RCTPerfMonitorCellIdentifier];
}
return _metrics;
}
- (void)show
{
if (_container != nullptr) {
return;
}
[self.container addSubview:self.memory];
[self.container addSubview:self.heap];
[self.container addSubview:self.views];
[self.container addSubview:self.uiGraph];
[self.container addSubview:self.uiGraphLabel];
[self redirectLogs];
RCTJSCSetOption("logGC=1");
[self updateStats];
[RCTKeyWindow() addSubview:self.container];
_uiDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)];
[_uiDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
self.container.frame =
(CGRect){self.container.frame.origin, {self.container.frame.size.width + 44, self.container.frame.size.height}};
[self.container addSubview:self.jsGraph];
[self.container addSubview:self.jsGraphLabel];
[_bridge
dispatchBlock:^{
self->_jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)];
[self->_jsDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
queue:RCTJSThread];
}
- (void)hide
{
if (_container == nullptr) {
return;
}
[self.container removeFromSuperview];
_container = nil;
_jsGraph = nil;
_uiGraph = nil;
RCTJSCSetOption("logGC=0");
[self stopLogs];
[_uiDisplayLink invalidate];
[_jsDisplayLink invalidate];
_uiDisplayLink = nil;
_jsDisplayLink = nil;
}
- (void)redirectLogs
{
_stderr = dup(STDERR_FILENO);
if (pipe(_pipe) != 0) {
return;
}
dup2(_pipe[1], STDERR_FILENO);
close(_pipe[1]);
__weak __typeof__(self) weakSelf = self;
_queue = dispatch_queue_create("com.facebook.react.RCTPerfMonitor", DISPATCH_QUEUE_SERIAL);
_io = dispatch_io_create(
DISPATCH_IO_STREAM,
_pipe[0],
_queue,
^(__unused int error){
});
dispatch_io_set_low_water(_io, 20);
dispatch_io_read(_io, 0, SIZE_MAX, _queue, ^(__unused bool done, dispatch_data_t data, __unused int error) {
if (data == nullptr) {
return;
}
dispatch_data_apply(
data, ^bool(__unused dispatch_data_t region, __unused size_t offset, const void *buffer, size_t size) {
write(self->_stderr, buffer, size);
NSString *log = [[NSString alloc] initWithBytes:buffer length:size encoding:NSUTF8StringEncoding];
[weakSelf parse:log];
return true;
});
});
}
- (void)stopLogs
{
dup2(_stderr, STDERR_FILENO);
dispatch_io_close(_io, 0);
}
- (void)parse:(NSString *)log
{
static NSRegularExpression *GCRegex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *pattern =
@"\\[GC: [\\d\\.]+ \\wb => (Eden|Full)Collection, (?:Skipped copying|Did copy), ([\\d\\.]+) \\wb, [\\d.]+ \\ws\\]";
GCRegex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
});
if (_remaining != nullptr) {
log = [_remaining stringByAppendingString:log];
_remaining = nil;
}
NSArray<NSString *> *lines = [log componentsSeparatedByString:@"\n"];
if (lines.count == 1) { // no newlines
_remaining = log;
return;
}
for (NSString *line in lines) {
NSTextCheckingResult *match = [GCRegex firstMatchInString:line options:0 range:NSMakeRange(0, line.length)];
if (match != nullptr) {
NSString *heapSizeStr = [line substringWithRange:[match rangeAtIndex:2]];
_heapSize = [heapSizeStr integerValue];
}
}
}
- (void)updateStats
{
NSDictionary<NSNumber *, UIView *> *views = [_bridge.uiManager valueForKey:@"viewRegistry"];
NSUInteger viewCount = views.count;
NSUInteger visibleViewCount = 0;
for (UIView *view in views.allValues) {
if ((view.window != nullptr) || (view.superview.window != nullptr)) {
visibleViewCount++;
}
}
// Ensure the container always stays on top of newly added views
if ([_container.superview.subviews lastObject] != _container) {
[_container.superview bringSubviewToFront:_container];
}
double mem = (double)RCTGetResidentMemorySize() / 1024 / 1024;
self.memory.text = [NSString stringWithFormat:@"RAM\n%.2lf\nMB", mem];
self.heap.text = [NSString stringWithFormat:@"JSC\n%.2lf\nMB", (double)_heapSize / 1024];
self.views.text =
[NSString stringWithFormat:@"Views\n%lu\n%lu", (unsigned long)visibleViewCount, (unsigned long)viewCount];
__weak __typeof__(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong __typeof__(weakSelf) strongSelf = weakSelf;
if ((strongSelf != nullptr) && (strongSelf->_container.superview != nullptr)) {
[strongSelf updateStats];
}
});
}
- (void)gesture:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint translation = [gestureRecognizer translationInView:self.container.superview];
self.container.center = CGPointMake(self.container.center.x + translation.x, self.container.center.y + translation.y);
[gestureRecognizer setTranslation:CGPointMake(0, 0) inView:self.container.superview];
}
- (void)tap
{
[self loadPerformanceLoggerData];
if (CGRectIsEmpty(_storedMonitorFrame)) {
UIEdgeInsets safeInsets = RCTKeyWindow().safeAreaInsets;
_storedMonitorFrame =
CGRectMake(safeInsets.left, safeInsets.top, self.container.window.frame.size.width, RCTPerfMonitorExpandHeight);
[self.container addSubview:self.metrics];
} else {
[_metrics reloadData];
}
[UIView animateWithDuration:.25
animations:^{
CGRect tmp = self.container.frame;
self.container.frame = self->_storedMonitorFrame;
self->_storedMonitorFrame = tmp;
}];
}
- (void)threadUpdate:(CADisplayLink *)displayLink
{
RCTFPSGraph *graph = displayLink == _jsDisplayLink ? _jsGraph : _uiGraph;
[graph onTick:displayLink.timestamp];
}
- (void)loadPerformanceLoggerData
{
NSUInteger i = 0;
NSMutableArray<NSString *> *data = [NSMutableArray new];
RCTPerformanceLogger *performanceLogger = [_bridge performanceLogger];
NSArray<NSNumber *> *values = [performanceLogger valuesForTags];
for (NSString *label in LabelsForRCTPerformanceLoggerTags()) {
long long value = values[i + 1].longLongValue - values[i].longLongValue;
NSString *unit = @"ms";
if ([label hasSuffix:@"Size"]) {
unit = @"b";
} else if ([label hasSuffix:@"Count"]) {
unit = @"";
}
[data addObject:[NSString stringWithFormat:@"%@: %lld%@", label, value, unit]];
i += 2;
}
_perfLoggerMarks = [data copy];
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(__unused NSInteger)section
{
return _perfLoggerMarks.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:RCTPerfMonitorCellIdentifier
forIndexPath:indexPath];
if (cell == nullptr) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:RCTPerfMonitorCellIdentifier];
}
cell.textLabel.text = _perfLoggerMarks[indexPath.row];
cell.textLabel.font = [UIFont systemFontOfSize:12];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(__unused UITableView *)tableView heightForRowAtIndexPath:(__unused NSIndexPath *)indexPath
{
return 20;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
@end
NSArray<NSString *> *LabelsForRCTPerformanceLoggerTags()
{
NSMutableArray<NSString *> *labels = [NSMutableArray new];
for (int i = 0; i < RCTPLSize; i++) {
[labels addObject:RCTPLLabelForTag((RCTPLTag)i)];
}
return labels;
}
#endif
Class RCTPerfMonitorCls(void)
{
#if RCT_DEV
return RCTPerfMonitor.class;
#else
return nil;
#endif
}

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface RCTPlatform : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,106 @@
/*
* 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.
*/
#import "RCTPlatform.h"
#import <UIKit/UIKit.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTInitializing.h>
#import <React/RCTUtils.h>
#import <React/RCTVersion.h>
#import "CoreModulesPlugins.h"
#import <optional>
using namespace facebook::react;
static NSString *interfaceIdiom(UIUserInterfaceIdiom idiom)
{
switch (idiom) {
case UIUserInterfaceIdiomPhone:
return @"phone";
case UIUserInterfaceIdiomPad:
return @"pad";
case UIUserInterfaceIdiomTV:
return @"tv";
case UIUserInterfaceIdiomCarPlay:
return @"carplay";
#if TARGET_OS_VISION
case UIUserInterfaceIdiomVision:
return @"vision";
#endif
default:
return @"unknown";
}
}
@interface RCTPlatform () <NativePlatformConstantsIOSSpec, RCTInitializing>
@end
@implementation RCTPlatform {
ModuleConstants<JS::NativePlatformConstantsIOS::Constants> _constants;
}
RCT_EXPORT_MODULE(PlatformConstants)
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (void)initialize
{
UIDevice *device = [UIDevice currentDevice];
auto versions = RCTGetReactNativeVersion();
_constants = typedConstants<JS::NativePlatformConstantsIOS::Constants>({
.forceTouchAvailable = RCTForceTouchAvailable() ? true : false,
.osVersion = [device systemVersion],
.systemName = [device systemName],
.interfaceIdiom = interfaceIdiom([device userInterfaceIdiom]),
.isTesting = RCTRunningInTestEnvironment() ? true : false,
.reactNativeVersion = JS::NativePlatformConstantsIOS::ConstantsReactNativeVersion::Builder(
{.minor = [versions[@"minor"] doubleValue],
.major = [versions[@"major"] doubleValue],
.patch = [versions[@"patch"] doubleValue],
.prerelease = [versions[@"prerelease"] isKindOfClass:[NSNull class]] ? nullptr : versions[@"prerelease"]}),
#if TARGET_OS_MACCATALYST
.isMacCatalyst = true,
#else
.isMacCatalyst = false,
#endif
});
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
// TODO: Use the generated struct return type.
- (ModuleConstants<JS::NativePlatformConstantsIOS::Constants>)constantsToExport
{
return _constants;
}
- (ModuleConstants<JS::NativePlatformConstantsIOS::Constants>)getConstants
{
return _constants;
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativePlatformConstantsIOSSpecJSI>(params);
}
@end
Class RCTPlatformCls(void)
{
return RCTPlatform.class;
}

View File

@@ -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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTErrorCustomizer.h>
@class RCTJSStackFrame;
typedef void (^RCTRedBoxButtonPressHandler)(void);
@interface RCTRedBox : NSObject <RCTBridgeModule>
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer;
- (void)showError:(NSError *)error;
- (void)showErrorMessage:(NSString *)message;
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details;
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack;
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack errorCookie:(int)errorCookie;
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack;
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack;
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie;
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie;
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack;
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack;
- (void)showErrorMessage:(NSString *)message
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
errorCookie:(int)errorCookie;
- (void)updateErrorMessage:(NSString *)message
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
errorCookie:(int)errorCookie;
- (void)dismiss;
- (void)addCustomButton:(NSString *)title onPressHandler:(RCTRedBoxButtonPressHandler)handler;
/** Overrides bridge.bundleURL. Modify on main thread only. You shouldn't need to use this. */
@property (nonatomic, strong) NSURL *overrideBundleURL;
/** Overrides the default behavior of calling [bridge reload] on reload. You shouldn't need to use this. */
@property (nonatomic, strong) dispatch_block_t overrideReloadAction;
@end

View File

@@ -0,0 +1,799 @@
/*
* 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.
*/
#import "RCTRedBox.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConvert.h>
#import <React/RCTDefines.h>
#import <React/RCTErrorInfo.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTJSStackFrame.h>
#import <React/RCTRedBoxExtraDataViewController.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTUtils.h>
#import <objc/runtime.h>
#import "CoreModulesPlugins.h"
#if RCT_DEV_MENU
@class RCTRedBoxController;
@interface UIButton (RCTRedBox)
@property (nonatomic) RCTRedBoxButtonPressHandler rct_handler;
- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents;
@end
@implementation UIButton (RCTRedBox)
- (RCTRedBoxButtonPressHandler)rct_handler
{
return objc_getAssociatedObject(self, @selector(rct_handler));
}
- (void)setRct_handler:(RCTRedBoxButtonPressHandler)rct_handler
{
objc_setAssociatedObject(self, @selector(rct_handler), rct_handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)rct_callBlock
{
if (self.rct_handler) {
self.rct_handler();
}
}
- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents
{
self.rct_handler = handler;
[self addTarget:self action:@selector(rct_callBlock) forControlEvents:controlEvents];
}
@end
@protocol RCTRedBoxControllerActionDelegate <NSObject>
- (void)redBoxController:(RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
- (void)reloadFromRedBoxController:(RCTRedBoxController *)redBoxController;
- (void)loadExtraDataViewController;
@end
@interface RCTRedBoxController : UIViewController <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, weak) id<RCTRedBoxControllerActionDelegate> actionDelegate;
@end
@implementation RCTRedBoxController {
UITableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray<RCTJSStackFrame *> *_lastStackTrace;
NSArray<NSString *> *_customButtonTitles;
NSArray<RCTRedBoxButtonPressHandler> *_customButtonHandlers;
int _lastErrorCookie;
}
- (instancetype)initWithCustomButtonTitles:(NSArray<NSString *> *)customButtonTitles
customButtonHandlers:(NSArray<RCTRedBoxButtonPressHandler> *)customButtonHandlers
{
if (self = [super init]) {
_lastErrorCookie = -1;
_customButtonTitles = customButtonTitles;
_customButtonHandlers = customButtonHandlers;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
const CGFloat buttonHeight = 60;
CGRect detailsFrame = self.view.bounds;
detailsFrame.size.height -= buttonHeight + (double)[self bottomSafeViewHeight];
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_stackTraceTableView.delegate = self;
_stackTraceTableView.dataSource = self;
_stackTraceTableView.backgroundColor = [UIColor clearColor];
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[self.view addSubview:_stackTraceTableView];
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
NSString *reloadText = @"Reload\n(\u2318R)";
NSString *dismissText = @"Dismiss\n(ESC)";
NSString *copyText = @"Copy\n(\u2325\u2318C)";
NSString *extraText = @"Extra Info\n(\u2318E)";
#else
NSString *reloadText = @"Reload JS";
NSString *dismissText = @"Dismiss";
NSString *copyText = @"Copy";
NSString *extraText = @"Extra Info";
#endif
UIButton *dismissButton = [self redBoxButton:dismissText
accessibilityIdentifier:@"redbox-dismiss"
selector:@selector(dismiss)
block:nil];
UIButton *reloadButton = [self redBoxButton:reloadText
accessibilityIdentifier:@"redbox-reload"
selector:@selector(reload)
block:nil];
UIButton *copyButton = [self redBoxButton:copyText
accessibilityIdentifier:@"redbox-copy"
selector:@selector(copyStack)
block:nil];
UIButton *extraButton = [self redBoxButton:extraText
accessibilityIdentifier:@"redbox-extra"
selector:@selector(showExtraDataViewController)
block:nil];
[NSLayoutConstraint activateConstraints:@[
[dismissButton.heightAnchor constraintEqualToConstant:buttonHeight],
[reloadButton.heightAnchor constraintEqualToConstant:buttonHeight],
[copyButton.heightAnchor constraintEqualToConstant:buttonHeight],
[extraButton.heightAnchor constraintEqualToConstant:buttonHeight]
]];
UIStackView *buttonStackView = [[UIStackView alloc] init];
buttonStackView.translatesAutoresizingMaskIntoConstraints = NO;
buttonStackView.axis = UILayoutConstraintAxisHorizontal;
buttonStackView.distribution = UIStackViewDistributionFillEqually;
buttonStackView.alignment = UIStackViewAlignmentTop;
buttonStackView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
[buttonStackView addArrangedSubview:dismissButton];
[buttonStackView addArrangedSubview:reloadButton];
[buttonStackView addArrangedSubview:copyButton];
[buttonStackView addArrangedSubview:extraButton];
[self.view addSubview:buttonStackView];
[NSLayoutConstraint activateConstraints:@[
[buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + [self bottomSafeViewHeight]],
[buttonStackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[buttonStackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[buttonStackView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
]];
for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) {
UIButton *button = [self redBoxButton:_customButtonTitles[i]
accessibilityIdentifier:@""
selector:nil
block:_customButtonHandlers[i]];
[button.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
[buttonStackView addArrangedSubview:button];
}
UIView *topBorder = [[UIView alloc] init];
topBorder.translatesAutoresizingMaskIntoConstraints = NO;
topBorder.backgroundColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
[topBorder.heightAnchor constraintEqualToConstant:1].active = YES;
[self.view addSubview:topBorder];
[NSLayoutConstraint activateConstraints:@[
[topBorder.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[topBorder.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[topBorder.bottomAnchor constraintEqualToAnchor:buttonStackView.topAnchor],
]];
}
- (UIButton *)redBoxButton:(NSString *)title
accessibilityIdentifier:(NSString *)accessibilityIdentifier
selector:(SEL)selector
block:(RCTRedBoxButtonPressHandler)block
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
button.accessibilityIdentifier = accessibilityIdentifier;
button.titleLabel.font = [UIFont systemFontOfSize:13];
button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
button.titleLabel.textAlignment = NSTextAlignmentCenter;
button.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
[button setTitle:title forState:UIControlStateNormal];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[button setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted];
if (selector) {
[button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
} else if (block) {
[button rct_addBlock:block forControlEvents:UIControlEventTouchUpInside];
}
return button;
}
- (NSInteger)bottomSafeViewHeight
{
#if TARGET_OS_MACCATALYST
return 0;
#else
return RCTKeyWindow().safeAreaInsets.bottom;
#endif
}
RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
- (NSString *)stripAnsi:(NSString *)text
{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\x1b\\[[0-9;]*m"
options:NSRegularExpressionCaseInsensitive
error:&error];
return [regex stringByReplacingMatchesInString:text options:0 range:NSMakeRange(0, [text length]) withTemplate:@""];
}
- (void)showErrorMessage:(NSString *)message
withStack:(NSArray<RCTJSStackFrame *> *)stack
isUpdate:(BOOL)isUpdate
errorCookie:(int)errorCookie
{
// Remove ANSI color codes from the message
NSString *messageWithoutAnsi = [self stripAnsi:message];
BOOL isRootViewControllerPresented = self.presentingViewController != nil;
// Show if this is a new message, or if we're updating the previous message
BOOL isNew = !isRootViewControllerPresented && !isUpdate;
BOOL isUpdateForSameMessage = !isNew &&
(isRootViewControllerPresented && isUpdate &&
((errorCookie == -1 && [_lastErrorMessage isEqualToString:messageWithoutAnsi]) ||
(errorCookie == _lastErrorCookie)));
if (isNew || isUpdateForSameMessage) {
_lastStackTrace = stack;
// message is displayed using UILabel, which is unable to render text of
// unlimited length, so we truncate it
_lastErrorMessage = [messageWithoutAnsi substringToIndex:MIN((NSUInteger)10000, messageWithoutAnsi.length)];
_lastErrorCookie = errorCookie;
[_stackTraceTableView reloadData];
if (!isRootViewControllerPresented) {
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[RCTKeyWindow().rootViewController presentViewController:self animated:YES completion:nil];
}
}
}
- (void)dismiss
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)reload
{
if (_actionDelegate != nil) {
[_actionDelegate reloadFromRedBoxController:self];
} else {
// In bridgeless mode `RCTRedBox` gets deallocated, we need to notify listeners anyway.
RCTTriggerReloadCommandListeners(@"Redbox");
[self dismiss];
}
}
- (void)showExtraDataViewController
{
[_actionDelegate loadExtraDataViewController];
}
- (void)copyStack
{
NSMutableString *fullStackTrace;
if (_lastErrorMessage != nil) {
fullStackTrace = [_lastErrorMessage mutableCopy];
[fullStackTrace appendString:@"\n\n"];
} else {
fullStackTrace = [NSMutableString string];
}
for (RCTJSStackFrame *stackFrame in _lastStackTrace) {
[fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]];
if (stackFrame.file) {
[fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]];
}
}
UIPasteboard *pb = [UIPasteboard generalPasteboard];
[pb setString:fullStackTrace];
}
- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
{
NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @"<unknown file>";
NSString *lineInfo = [NSString stringWithFormat:@"%@:%lld", fileName, (long long)stackFrame.lineNumber];
if (stackFrame.column != 0) {
lineInfo = [lineInfo stringByAppendingFormat:@":%lld", (long long)stackFrame.column];
}
return lineInfo;
}
#pragma mark - TableView
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView
{
return 2;
}
- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return section == 0 ? 1 : _lastStackTrace.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
return [self reuseCell:cell forErrorMessage:_lastErrorMessage];
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
NSUInteger index = indexPath.row;
RCTJSStackFrame *stackFrame = _lastStackTrace[index];
return [self reuseCell:cell forStackFrame:stackFrame];
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"];
cell.textLabel.accessibilityIdentifier = @"redbox-error";
cell.textLabel.textColor = [UIColor whiteColor];
// Prefer a monofont for formatting messages that were designed
// to be displayed in a terminal.
cell.textLabel.font = [UIFont monospacedSystemFontOfSize:14 weight:UIFontWeightBold];
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.textLabel.numberOfLines = 0;
cell.detailTextLabel.textColor = [UIColor whiteColor];
cell.backgroundColor = [UIColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
cell.textLabel.text = message;
return cell;
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14];
cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping;
cell.textLabel.numberOfLines = 2;
cell.detailTextLabel.textColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11];
cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
cell.backgroundColor = [UIColor clearColor];
cell.selectedBackgroundView = [UIView new];
cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2];
}
cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)";
if (stackFrame.file) {
cell.detailTextLabel.text = [self formatFrameSource:stackFrame];
} else {
cell.detailTextLabel.text = @"";
}
cell.textLabel.textColor = stackFrame.collapse ? [UIColor lightGrayColor] : [UIColor whiteColor];
cell.detailTextLabel.textColor = stackFrame.collapse ? [UIColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.0]
: [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes =
@{NSFontAttributeName : [UIFont boldSystemFontOfSize:16], NSParagraphStyleAttributeName : paragraphStyle};
CGRect boundingRect =
[_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil];
return ceil(boundingRect.size.height) + 40;
} else {
return 50;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 1) {
NSUInteger row = indexPath.row;
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
[_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - Key commands
- (NSArray<UIKeyCommand *> *)keyCommands
{
// NOTE: We could use RCTKeyCommands for this, but since
// we control this window, we can use the standard, non-hacky
// mechanism instead
return @[
// Dismiss red box
[UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(dismiss)],
// Reload
[UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:@selector(reload)],
// Copy = Cmd-Option C since Cmd-C in the simulator copies the pasteboard from
// the simulator to the desktop pasteboard.
[UIKeyCommand keyCommandWithInput:@"c"
modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate
action:@selector(copyStack)],
// Extra data
[UIKeyCommand keyCommandWithInput:@"e"
modifierFlags:UIKeyModifierCommand
action:@selector(showExtraDataViewController)]
];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end
@interface RCTRedBox () <
RCTInvalidating,
RCTRedBoxControllerActionDelegate,
RCTRedBoxExtraDataActionDelegate,
NativeRedBoxSpec>
@end
@implementation RCTRedBox {
RCTRedBoxController *_controller;
NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
RCTRedBoxExtraDataViewController *_extraDataViewController;
NSMutableArray<NSString *> *_customButtonTitles;
NSMutableArray<RCTRedBoxButtonPressHandler> *_customButtonHandlers;
}
@synthesize bridge = _bridge;
@synthesize moduleRegistry = _moduleRegistry;
@synthesize bundleManager = _bundleManager;
RCT_EXPORT_MODULE()
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!self->_errorCustomizers) {
self->_errorCustomizers = [NSMutableArray array];
}
if (![self->_errorCustomizers containsObject:errorCustomizer]) {
[self->_errorCustomizers addObject:errorCustomizer];
}
});
}
// WARNING: Should only be called from the main thread/dispatch queue.
- (RCTErrorInfo *)_customizeError:(RCTErrorInfo *)error
{
RCTAssertMainQueue();
if (!self->_errorCustomizers) {
return error;
}
for (id<RCTErrorCustomizer> customizer in self->_errorCustomizers) {
RCTErrorInfo *newInfo = [customizer customizeErrorInfo:error];
if (newInfo) {
error = newInfo;
}
}
return error;
}
- (void)showError:(NSError *)error
{
[self showErrorMessage:error.localizedDescription
withDetails:error.localizedFailureReason
stack:error.userInfo[RCTJSStackTraceKey]
errorCookie:-1];
}
- (void)showErrorMessage:(NSString *)message
{
[self showErrorMessage:message withParsedStack:nil isUpdate:NO errorCookie:-1];
}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
{
[self showErrorMessage:message withDetails:details stack:nil errorCookie:-1];
}
- (void)showErrorMessage:(NSString *)message
withDetails:(NSString *)details
stack:(NSArray<RCTJSStackFrame *> *)stack
errorCookie:(int)errorCookie
{
NSString *combinedMessage = message;
if (details) {
combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details];
}
[self showErrorMessage:combinedMessage withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack
{
[self showErrorMessage:message withRawStack:rawStack errorCookie:-1];
}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack errorCookie:(int)errorCookie
{
NSArray<RCTJSStackFrame *> *stack = [RCTJSStackFrame stackFramesWithLines:rawStack];
[self showErrorMessage:message withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self showErrorMessage:message withStack:stack errorCookie:-1];
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self updateErrorMessage:message withStack:stack errorCookie:-1];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
{
[self showErrorMessage:message
withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack]
isUpdate:NO
errorCookie:errorCookie];
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
{
[self showErrorMessage:message
withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack]
isUpdate:YES
errorCookie:errorCookie];
}
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
{
[self showErrorMessage:message withParsedStack:stack errorCookie:-1];
}
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
{
[self updateErrorMessage:message withParsedStack:stack errorCookie:-1];
}
- (void)showErrorMessage:(NSString *)message
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
errorCookie:(int)errorCookie
{
[self showErrorMessage:message withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
}
- (void)updateErrorMessage:(NSString *)message
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
errorCookie:(int)errorCookie
{
[self showErrorMessage:message withParsedStack:stack isUpdate:YES errorCookie:errorCookie];
}
- (void)showErrorMessage:(NSString *)message
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
isUpdate:(BOOL)isUpdate
errorCookie:(int)errorCookie
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_extraDataViewController == nil) {
self->_extraDataViewController = [RCTRedBoxExtraDataViewController new];
self->_extraDataViewController.actionDelegate = self;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[self->_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"collectRedBoxExtraData"
body:nil];
#pragma clang diagnostic pop
if (!self->_controller) {
self->_controller = [[RCTRedBoxController alloc] initWithCustomButtonTitles:self->_customButtonTitles
customButtonHandlers:self->_customButtonHandlers];
self->_controller.actionDelegate = self;
}
RCTErrorInfo *errorInfo = [[RCTErrorInfo alloc] initWithErrorMessage:message stack:stack];
errorInfo = [self _customizeError:errorInfo];
[self->_controller showErrorMessage:errorInfo.errorMessage
withStack:errorInfo.stack
isUpdate:isUpdate
errorCookie:errorCookie];
});
}
- (void)loadExtraDataViewController
{
dispatch_async(dispatch_get_main_queue(), ^{
// Make sure the CMD+E shortcut doesn't call this twice
if (self->_extraDataViewController != nil && ![self->_controller presentedViewController]) {
[self->_controller presentViewController:self->_extraDataViewController animated:YES completion:nil];
}
});
}
RCT_EXPORT_METHOD(setExtraData : (NSDictionary *)extraData forIdentifier : (NSString *)identifier)
{
[_extraDataViewController addExtraData:extraData forIdentifier:identifier];
}
RCT_EXPORT_METHOD(dismiss)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_controller dismiss];
});
}
- (void)invalidate
{
[self dismiss];
}
- (void)redBoxController:(__unused RCTRedBoxController *)redBoxController
openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
{
NSURL *const bundleURL = _overrideBundleURL ?: _bundleManager.bundleURL;
if (![bundleURL.scheme hasPrefix:@"http"]) {
RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager.");
return;
}
NSData *stackFrameJSON = [RCTJSONStringify([stackFrame toDictionary], NULL) dataUsingEncoding:NSUTF8StringEncoding];
NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length];
NSMutableURLRequest *request = [NSMutableURLRequest new];
request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:bundleURL];
request.HTTPMethod = @"POST";
request.HTTPBody = stackFrameJSON;
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];
}
- (void)reload
{
// Window is not used and can be nil
[self reloadFromRedBoxController:nil];
}
- (void)reloadFromRedBoxController:(__unused RCTRedBoxController *)redBoxController
{
if (_overrideReloadAction) {
_overrideReloadAction();
} else {
RCTTriggerReloadCommandListeners(@"Redbox");
}
[self dismiss];
}
- (void)addCustomButton:(NSString *)title onPressHandler:(RCTRedBoxButtonPressHandler)handler
{
if (!_customButtonTitles) {
_customButtonTitles = [NSMutableArray new];
_customButtonHandlers = [NSMutableArray new];
}
[_customButtonTitles addObject:title];
[_customButtonHandlers addObject:handler];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeRedBoxSpecJSI>(params);
}
@end
#else // Disabled
@interface RCTRedBox () <NativeRedBoxSpec>
@end
@implementation RCTRedBox
+ (NSString *)moduleName
{
return nil;
}
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer
{
}
- (void)showError:(NSError *)error
{
}
- (void)showErrorMessage:(NSString *)message
{
}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
{
}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack
{
}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack errorCookie:(int)errorCookie
{
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
{
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
{
}
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
{
}
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
{
}
- (void)showErrorMessage:(NSString *)message
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
errorCookie:(int)errorCookie
{
}
- (void)updateErrorMessage:(NSString *)message
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
errorCookie:(int)errorCookie
{
}
- (void)setExtraData:(NSDictionary *)extraData forIdentifier:(NSString *)identifier
{
}
- (void)dismiss
{
}
- (void)addCustomButton:(NSString *)title onPressHandler:(RCTRedBoxButtonPressHandler)handler
{
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeRedBoxSpecJSI>(params);
}
@end
#endif
Class RCTRedBoxCls(void)
{
return RCTRedBox.class;
}

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface RCTSourceCode : NSObject <RCTBridgeModule>
@end

View File

@@ -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.
*/
#import "RCTSourceCode.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import "CoreModulesPlugins.h"
using namespace facebook::react;
@interface RCTSourceCode () <NativeSourceCodeSpec>
@end
@implementation RCTSourceCode
RCT_EXPORT_MODULE()
@synthesize bundleManager = _bundleManager;
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (NSDictionary<NSString *, id> *)constantsToExport
{
return [self getConstants];
}
- (NSDictionary<NSString *, id> *)getConstants
{
return @{
@"scriptURL" : self.bundleManager.bundleURL.absoluteString ?: @"",
};
}
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeSourceCodeSpecJSI>(params);
}
@end
Class RCTSourceCodeCls(void)
{
return RCTSourceCode.class;
}

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#import <React/RCTConvert.h>
#import <React/RCTEventEmitter.h>
@interface RCTConvert (UIStatusBar)
+ (UIStatusBarStyle)UIStatusBarStyle:(id)json;
+ (UIStatusBarAnimation)UIStatusBarAnimation:(id)json;
@end
@interface RCTStatusBarManager : RCTEventEmitter
@end

View File

@@ -0,0 +1,210 @@
/*
* 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.
*/
#import "RCTStatusBarManager.h"
#import "CoreModulesPlugins.h"
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTInitializing.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
static NSString *const kStatusBarFrameDidChange = @"statusBarFrameDidChange";
static NSString *const kStatusBarFrameWillChange = @"statusBarFrameWillChange";
@implementation RCTConvert (UIStatusBar)
+ (UIStatusBarStyle)UIStatusBarStyle:(id)json RCT_DYNAMIC
{
static NSDictionary *mapping;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mapping = @{
@"default" : @(UIStatusBarStyleDefault),
@"light-content" : @(UIStatusBarStyleLightContent),
@"dark-content" : @(UIStatusBarStyleDarkContent)
};
});
return _RCT_CAST(
UIStatusBarStyle,
[RCTConvertEnumValue("UIStatusBarStyle", mapping, @(UIStatusBarStyleDefault), json) integerValue]);
}
RCT_ENUM_CONVERTER(
UIStatusBarAnimation,
(@{
@"none" : @(UIStatusBarAnimationNone),
@"fade" : @(UIStatusBarAnimationFade),
@"slide" : @(UIStatusBarAnimationSlide),
}),
UIStatusBarAnimationNone,
integerValue);
@end
@interface RCTStatusBarManager () <NativeStatusBarManagerIOSSpec, RCTInitializing>
@end
@implementation RCTStatusBarManager {
facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants> _constants;
}
static BOOL RCTViewControllerBasedStatusBarAppearance()
{
static BOOL value;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
value =
[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]
?: @YES boolValue];
});
return value;
}
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (void)initialize
{
_constants = facebook::react::typedConstants<JS::NativeStatusBarManagerIOS::Constants>({
.HEIGHT = RCTUIStatusBarManager().statusBarFrame.size.height,
.DEFAULT_BACKGROUND_COLOR = std::nullopt,
});
}
- (NSArray<NSString *> *)supportedEvents
{
return @[ kStatusBarFrameDidChange, kStatusBarFrameWillChange ];
}
- (void)startObserving
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[nc addObserver:self
selector:@selector(applicationDidChangeStatusBarFrame:)
name:UIApplicationDidChangeStatusBarFrameNotification
object:nil];
[nc addObserver:self
selector:@selector(applicationWillChangeStatusBarFrame:)
name:UIApplicationWillChangeStatusBarFrameNotification
object:nil];
#pragma clang diagnostic pop
}
- (void)stopObserving
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)emitEvent:(NSString *)eventName forNotification:(NSNotification *)notification
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CGRect frame = [notification.userInfo[UIApplicationStatusBarFrameUserInfoKey] CGRectValue];
#pragma clang diagnostic pop
NSDictionary *event = @{
@"frame" : @{
@"x" : @(frame.origin.x),
@"y" : @(frame.origin.y),
@"width" : @(frame.size.width),
@"height" : @(frame.size.height),
},
};
[self sendEventWithName:eventName body:event];
}
- (void)applicationDidChangeStatusBarFrame:(NSNotification *)notification
{
[self emitEvent:kStatusBarFrameDidChange forNotification:notification];
}
- (void)applicationWillChangeStatusBarFrame:(NSNotification *)notification
{
[self emitEvent:kStatusBarFrameWillChange forNotification:notification];
}
RCT_EXPORT_METHOD(getHeight : (RCTResponseSenderBlock)callback)
{
callback(@[ @{
@"height" : @(RCTUIStatusBarManager().statusBarFrame.size.height),
} ]);
}
RCT_EXPORT_METHOD(setStyle : (NSString *)style animated : (BOOL)animated)
{
dispatch_async(dispatch_get_main_queue(), ^{
UIStatusBarStyle statusBarStyle = [RCTConvert UIStatusBarStyle:style];
if (RCTViewControllerBasedStatusBarAppearance()) {
RCTLogError(@"RCTStatusBarManager module requires that the \
UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO");
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[RCTSharedApplication() setStatusBarStyle:statusBarStyle animated:animated];
}
#pragma clang diagnostic pop
});
}
RCT_EXPORT_METHOD(setHidden : (BOOL)hidden withAnimation : (NSString *)withAnimation)
{
dispatch_async(dispatch_get_main_queue(), ^{
UIStatusBarAnimation animation = [RCTConvert UIStatusBarAnimation:withAnimation];
if (RCTViewControllerBasedStatusBarAppearance()) {
RCTLogError(@"RCTStatusBarManager module requires that the \
UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO");
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[RCTSharedApplication() setStatusBarHidden:hidden withAnimation:animation];
#pragma clang diagnostic pop
}
});
}
RCT_EXPORT_METHOD(setNetworkActivityIndicatorVisible : (BOOL)visible)
{
dispatch_async(dispatch_get_main_queue(), ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// This is no longer supported in iOS 13 and later. We will remove this method in a future release.
RCTSharedApplication().networkActivityIndicatorVisible = visible;
#pragma clang diagnostic pop
});
}
- (facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants>)getConstants
{
return _constants;
}
- (facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants>)constantsToExport
{
return (facebook::react::ModuleConstants<JS::NativeStatusBarManagerIOS::Constants>)[self getConstants];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeStatusBarManagerIOSSpecJSI>(params);
}
@end
Class RCTStatusBarManagerCls(void)
{
return RCTStatusBarManager.class;
}

View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTFrameUpdate.h>
#import <React/RCTInitializing.h>
#import <React/RCTInvalidating.h>
NS_ASSUME_NONNULL_BEGIN
@protocol RCTTimingDelegate
- (void)callTimers:(NSArray<NSNumber *> *)timers;
- (void)immediatelyCallTimer:(NSNumber *)callbackID;
- (void)callIdleCallbacks:(NSNumber *)absoluteFrameStartMS;
@end
@interface RCTTiming : NSObject <RCTBridgeModule, RCTInvalidating, RCTFrameUpdateObserver, RCTInitializing>
- (instancetype)initWithDelegate:(id<RCTTimingDelegate>)delegate;
- (void)createTimerForNextFrame:(NSNumber *)callbackID
duration:(NSTimeInterval)jsDuration
jsSchedulingTime:(nullable NSDate *)jsSchedulingTime
repeats:(BOOL)repeats;
- (void)deleteTimer:(double)timerID;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,435 @@
/*
* 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.
*/
#import "RCTTiming.h"
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTAssert.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
static const NSTimeInterval kMinimumSleepInterval = 1;
// These timing contants should be kept in sync with the ones in `JSTimers.js`.
// The duration of a frame. This assumes that we want to run at 60 fps.
static const NSTimeInterval kFrameDuration = 1.0 / 60.0;
// The minimum time left in a frame to trigger the idle callback.
static const NSTimeInterval kIdleCallbackFrameDeadline = 0.001;
@interface _RCTTimer : NSObject
@property (nonatomic, strong, readonly) NSDate *target;
@property (nonatomic, assign, readonly) BOOL repeats;
@property (nonatomic, copy, readonly) NSNumber *callbackID;
@property (nonatomic, assign, readonly) NSTimeInterval interval;
@end
@implementation _RCTTimer
- (instancetype)initWithCallbackID:(NSNumber *)callbackID
interval:(NSTimeInterval)interval
targetTime:(NSTimeInterval)targetTime
repeats:(BOOL)repeats
{
if ((self = [super init])) {
_interval = interval;
_repeats = repeats;
_callbackID = callbackID;
_target = [NSDate dateWithTimeIntervalSinceNow:targetTime];
}
return self;
}
/**
* Returns `YES` if we should invoke the JS callback.
*/
- (BOOL)shouldFire:(NSDate *)now
{
if (_target && [_target timeIntervalSinceDate:now] <= 0) {
return YES;
}
return NO;
}
- (void)reschedule
{
// The JS Timers will do fine grained calculating of expired timeouts.
_target = [NSDate dateWithTimeIntervalSinceNow:_interval];
}
@end
@interface _RCTTimingProxy : NSObject
@end
// NSTimer retains its target, insert this class to break potential retain cycles
@implementation _RCTTimingProxy {
__weak id _target;
}
+ (instancetype)proxyWithTarget:(id)target
{
_RCTTimingProxy *proxy = [self new];
if (proxy) {
proxy->_target = target;
}
return proxy;
}
- (void)timerDidFire
{
[_target timerDidFire];
}
@end
@implementation RCTTiming {
NSMutableDictionary<NSNumber *, _RCTTimer *> *_timers;
NSTimer *_sleepTimer;
BOOL _sendIdleEvents;
BOOL _inBackground;
id<RCTTimingDelegate> _timingDelegate;
}
@synthesize bridge = _bridge;
@synthesize paused = _paused;
@synthesize pauseCallback = _pauseCallback;
RCT_EXPORT_MODULE()
- (instancetype)initWithDelegate:(id<RCTTimingDelegate>)delegate
{
if (self = [super init]) {
[self setup];
_timingDelegate = delegate;
}
return self;
}
- (void)initialize
{
[self setup];
}
- (void)setup
{
_paused = YES;
_timers = [NSMutableDictionary new];
_inBackground = NO;
RCTExecuteOnMainQueue(^{
if (!self->_inBackground &&
([RCTSharedApplication() applicationState] == UIApplicationStateBackground ||
[UIDevice currentDevice].proximityState)) {
[self appDidMoveToBackground];
}
});
for (NSString *name in @[
UIApplicationWillResignActiveNotification,
UIApplicationDidEnterBackgroundNotification,
UIApplicationWillTerminateNotification
]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidMoveToBackground)
name:name
object:nil];
}
for (NSString *name in @[ UIApplicationDidBecomeActiveNotification, UIApplicationWillEnterForegroundNotification ]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidMoveToForeground)
name:name
object:nil];
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(proximityChanged)
name:UIDeviceProximityStateDidChangeNotification
object:nil];
}
- (void)dealloc
{
[_sleepTimer invalidate];
}
- (dispatch_queue_t)methodQueue
{
return RCTJSThread;
}
- (void)invalidate
{
[self stopTimers];
_bridge = nil;
_timingDelegate = nil;
}
- (void)appDidMoveToBackground
{
// Deactivate the CADisplayLink while in the background.
[self stopTimers];
_inBackground = YES;
// Issue one final timer callback, which will schedule a
// background NSTimer, if needed.
[self didUpdateFrame:nil];
}
- (void)appDidMoveToForeground
{
_inBackground = NO;
[self startTimers];
}
- (void)proximityChanged
{
BOOL isClose = [UIDevice currentDevice].proximityState;
if (isClose) {
[self appDidMoveToBackground];
} else {
[self appDidMoveToForeground];
}
}
- (void)stopTimers
{
if (_inBackground) {
return;
}
if (!_paused) {
_paused = YES;
if (_pauseCallback) {
_pauseCallback();
}
}
}
- (void)startTimers
{
if ((!_bridge && !_timingDelegate) || _inBackground || ![self hasPendingTimers]) {
return;
}
if (_paused) {
_paused = NO;
if (_pauseCallback) {
_pauseCallback();
}
}
}
- (BOOL)hasPendingTimers
{
@synchronized(_timers) {
return _sendIdleEvents || _timers.count > 0;
}
}
- (void)didUpdateFrame:(RCTFrameUpdate *)update
{
NSDate *nextScheduledTarget = [NSDate distantFuture];
NSMutableArray<_RCTTimer *> *timersToCall = [NSMutableArray new];
NSDate *now = [NSDate date]; // compare all the timers to the same base time
@synchronized(_timers) {
for (_RCTTimer *timer in _timers.allValues) {
if ([timer shouldFire:now]) {
[timersToCall addObject:timer];
} else {
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
}
}
}
// Call timers that need to be called
if (timersToCall.count > 0) {
NSArray<NSNumber *> *sortedTimers = [[timersToCall sortedArrayUsingComparator:^(_RCTTimer *a, _RCTTimer *b) {
return [a.target compare:b.target];
}] valueForKey:@"callbackID"];
if (_bridge) {
[_bridge enqueueJSCall:@"JSTimers" method:@"callTimers" args:@[ sortedTimers ] completion:NULL];
} else {
[_timingDelegate callTimers:sortedTimers];
}
}
for (_RCTTimer *timer in timersToCall) {
if (timer.repeats) {
[timer reschedule];
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
} else {
@synchronized(_timers) {
[_timers removeObjectForKey:timer.callbackID];
}
}
}
if (_sendIdleEvents) {
NSTimeInterval frameElapsed = (CACurrentMediaTime() - update.timestamp);
if (kFrameDuration - frameElapsed >= kIdleCallbackFrameDeadline) {
NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
NSNumber *absoluteFrameStartMS = @((currentTimestamp - frameElapsed) * 1000);
if (_bridge) {
[_bridge enqueueJSCall:@"JSTimers" method:@"callIdleCallbacks" args:@[ absoluteFrameStartMS ] completion:NULL];
} else {
[_timingDelegate callIdleCallbacks:absoluteFrameStartMS];
}
}
}
// Switch to a paused state only if we didn't call any timer this frame, so if
// in response to this timer another timer is scheduled, we don't pause and unpause
// the displaylink frivolously.
NSUInteger timerCount;
@synchronized(_timers) {
timerCount = _timers.count;
}
if (_inBackground) {
if (timerCount) {
[self scheduleSleepTimer:nextScheduledTarget];
}
} else if (!_sendIdleEvents && timersToCall.count == 0) {
// No need to call the pauseCallback as RCTDisplayLink will ask us about our paused
// status immediately after completing this call
if (timerCount == 0) {
_paused = YES;
}
// If the next timer is more than 1 second out, pause and schedule an NSTimer;
else if ([nextScheduledTarget timeIntervalSinceNow] > kMinimumSleepInterval) {
[self scheduleSleepTimer:nextScheduledTarget];
_paused = YES;
}
}
}
- (void)scheduleSleepTimer:(NSDate *)sleepTarget
{
@synchronized(self) {
if (!_sleepTimer || !_sleepTimer.valid) {
_sleepTimer = [[NSTimer alloc] initWithFireDate:sleepTarget
interval:0
target:[_RCTTimingProxy proxyWithTarget:self]
selector:@selector(timerDidFire)
userInfo:nil
repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:_sleepTimer forMode:NSDefaultRunLoopMode];
} else {
_sleepTimer.fireDate = [_sleepTimer.fireDate earlierDate:sleepTarget];
}
}
}
- (void)timerDidFire
{
_sleepTimer = nil;
if (_paused) {
[self startTimers];
// Immediately dispatch frame, so we don't have to wait on the displaylink.
[self didUpdateFrame:nil];
}
}
/**
* A method used for asynchronously creating a timer. If the timer has already expired,
* (based on the provided jsSchedulingTime) then it will be immediately invoked.
*
* There's a small difference between the time when we call
* setTimeout/setInterval/requestAnimation frame and the time it actually makes
* it here. This is important and needs to be taken into account when
* calculating the timer's target time. We calculate this by passing in
* Date.now() from JS and then subtracting that from the current time here.
*/
RCT_EXPORT_METHOD(
createTimer : (double)callbackID duration : (NSTimeInterval)jsDuration jsSchedulingTime : (double)
jsSchedulingTime repeats : (BOOL)repeats)
{
NSNumber *callbackIdObjc = [NSNumber numberWithDouble:callbackID];
NSDate *schedulingTime = [RCTConvert NSDate:[NSNumber numberWithDouble:jsSchedulingTime]];
if (jsDuration == 0 && repeats == NO) {
// For super fast, one-off timers, just enqueue them immediately rather than waiting a frame.
if (_bridge) {
[_bridge _immediatelyCallTimer:callbackIdObjc];
} else {
[_timingDelegate immediatelyCallTimer:callbackIdObjc];
}
return;
}
[self createTimerForNextFrame:callbackIdObjc duration:jsDuration jsSchedulingTime:schedulingTime repeats:repeats];
}
/**
* A method used for synchronously creating a timer. The timer will not be invoked until the
* next frame, regardless of whether it has already expired (i.e. jsSchedulingTime is 0).
*/
- (void)createTimerForNextFrame:(nonnull NSNumber *)callbackID
duration:(NSTimeInterval)jsDuration
jsSchedulingTime:(NSDate *)jsSchedulingTime
repeats:(BOOL)repeats
{
NSTimeInterval jsSchedulingOverhead = MAX(-jsSchedulingTime.timeIntervalSinceNow, 0);
NSTimeInterval targetTime = jsDuration - jsSchedulingOverhead;
if (jsDuration < 0.018) { // Make sure short intervals run each frame
jsDuration = 0;
}
_RCTTimer *timer = [[_RCTTimer alloc] initWithCallbackID:callbackID
interval:jsDuration
targetTime:targetTime
repeats:repeats];
@synchronized(_timers) {
_timers[callbackID] = timer;
}
if (_inBackground) {
[self scheduleSleepTimer:timer.target];
} else if (_paused) {
if ([timer.target timeIntervalSinceNow] > kMinimumSleepInterval) {
[self scheduleSleepTimer:timer.target];
} else {
[self startTimers];
}
}
}
RCT_EXPORT_METHOD(deleteTimer : (double)timerID)
{
@synchronized(_timers) {
[_timers removeObjectForKey:[NSNumber numberWithDouble:timerID]];
}
if (![self hasPendingTimers]) {
[self stopTimers];
}
}
RCT_EXPORT_METHOD(setSendIdleEvents : (BOOL)sendIdleEvents)
{
_sendIdleEvents = sendIdleEvents;
if (sendIdleEvents) {
[self startTimers];
} else if (![self hasPendingTimers]) {
[self stopTimers];
}
}
@end
Class RCTTimingCls(void)
{
return RCTTiming.class;
}

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
#import <React/RCTBridgeProxy.h>
#import <React/RCTEventEmitter.h>
NS_ASSUME_NONNULL_BEGIN
@protocol RCTWebSocketContentHandler <NSObject>
- (id)processWebsocketMessage:(id __nullable)message
forSocketID:(NSNumber *)socketID
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
@end
@interface RCTWebSocketModule : RCTEventEmitter
// Register a custom handler for a specific websocket. The handler will be strongly held by the WebSocketModule.
- (void)setContentHandler:(id<RCTWebSocketContentHandler> __nullable)handler forSocketID:(NSNumber *)socketID;
- (void)sendData:(NSData *)data forSocketID:(nonnull NSNumber *)socketID;
@end
@interface RCTBridge (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule;
@end
@interface RCTBridgeProxy (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,220 @@
/*
* 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.
*/
#import <React/RCTWebSocketModule.h>
#import <objc/runtime.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConvert.h>
#import <React/RCTUtils.h>
#import <SocketRocket/SRWebSocket.h>
#import "CoreModulesPlugins.h"
@implementation SRWebSocket (React)
- (NSNumber *)reactTag
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setReactTag:(NSNumber *)reactTag
{
objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
@interface RCTWebSocketModule () <SRWebSocketDelegate, NativeWebSocketModuleSpec>
@end
@implementation RCTWebSocketModule {
NSMutableDictionary<NSNumber *, SRWebSocket *> *_sockets;
NSMutableDictionary<NSNumber *, id<RCTWebSocketContentHandler>> *_contentHandlers;
}
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (NSArray *)supportedEvents
{
return @[ @"websocketMessage", @"websocketOpen", @"websocketFailed", @"websocketClosed" ];
}
- (void)invalidate
{
[super invalidate];
_contentHandlers = nil;
for (SRWebSocket *socket in _sockets.allValues) {
socket.delegate = nil;
[socket close];
}
}
RCT_EXPORT_METHOD(
connect : (NSURL *)URL protocols : (NSArray *)protocols options : (JS::NativeWebSocketModule::SpecConnectOptions &)
options socketID : (double)socketID)
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
// We load cookies from sharedHTTPCookieStorage (shared with XHR and
// fetch). To get secure cookies for wss URLs, replace wss with https
// in the URL.
NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:true];
if ([components.scheme.lowercaseString isEqualToString:@"wss"]) {
components.scheme = @"https";
}
// Load and set the cookie header.
NSArray<NSHTTPCookie *> *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:components.URL];
request.allHTTPHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
// Load supplied headers
if ([options.headers() isKindOfClass:NSDictionary.class]) {
NSDictionary *headers = (NSDictionary *)options.headers();
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
[request addValue:[RCTConvert NSString:value] forHTTPHeaderField:key];
}];
}
SRWebSocket *webSocket = [[SRWebSocket alloc] initWithURLRequest:request protocols:protocols];
[webSocket setDelegateDispatchQueue:[self methodQueue]];
webSocket.delegate = self;
webSocket.reactTag = @(socketID);
if (!_sockets) {
_sockets = [NSMutableDictionary new];
}
_sockets[@(socketID)] = webSocket;
[webSocket open];
}
RCT_EXPORT_METHOD(send : (NSString *)message forSocketID : (double)socketID)
{
[_sockets[@(socketID)] sendString:message error:nil];
}
RCT_EXPORT_METHOD(sendBinary : (NSString *)base64String forSocketID : (double)socketID)
{
[self sendData:[[NSData alloc] initWithBase64EncodedString:base64String options:0] forSocketID:@(socketID)];
}
- (void)sendData:(NSData *)data forSocketID:(NSNumber *__nonnull)socketID
{
[_sockets[socketID] sendData:data error:nil];
}
RCT_EXPORT_METHOD(ping : (double)socketID)
{
[_sockets[@(socketID)] sendPing:nil error:nil];
}
RCT_EXPORT_METHOD(close : (double)code reason : (NSString *)reason socketID : (double)socketID)
{
[_sockets[@(socketID)] closeWithCode:code reason:reason];
[_sockets removeObjectForKey:@(socketID)];
}
- (void)setContentHandler:(id<RCTWebSocketContentHandler>)handler forSocketID:(NSString *)socketID
{
if (!_contentHandlers) {
_contentHandlers = [NSMutableDictionary new];
}
_contentHandlers[socketID] = handler;
}
#pragma mark - RCTSRWebSocketDelegate methods
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSString *type;
NSNumber *socketID = [webSocket reactTag];
id contentHandler = _contentHandlers[socketID];
if (contentHandler) {
message = [contentHandler processWebsocketMessage:message forSocketID:socketID withType:&type];
} else {
if ([message isKindOfClass:[NSData class]]) {
type = @"binary";
message = [message base64EncodedStringWithOptions:0];
} else {
type = @"text";
}
}
[self sendEventWithName:@"websocketMessage" body:@{@"data" : message, @"type" : type, @"id" : webSocket.reactTag}];
}
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
[self sendEventWithName:@"websocketOpen"
body:@{@"id" : webSocket.reactTag, @"protocol" : webSocket.protocol ? webSocket.protocol : @""}];
}
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
NSNumber *socketID = [webSocket reactTag];
_contentHandlers[socketID] = nil;
_sockets[socketID] = nil;
NSDictionary *body =
@{@"message" : error.localizedDescription ?: @"Undefined, error is nil", @"id" : socketID ?: @(-1)};
[self sendEventWithName:@"websocketFailed" body:body];
}
- (void)webSocket:(SRWebSocket *)webSocket
didCloseWithCode:(NSInteger)code
reason:(NSString *)reason
wasClean:(BOOL)wasClean
{
NSNumber *socketID = [webSocket reactTag];
_contentHandlers[socketID] = nil;
_sockets[socketID] = nil;
[self sendEventWithName:@"websocketClosed"
body:@{
@"code" : @(code),
@"reason" : RCTNullIfNil(reason),
@"clean" : @(wasClean),
@"id" : socketID
}];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeWebSocketModuleSpecJSI>(params);
}
@end
@implementation RCTBridge (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule
{
return [self moduleForClass:[RCTWebSocketModule class]];
}
@end
@implementation RCTBridgeProxy (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule
{
return [self moduleForClass:[RCTWebSocketModule class]];
}
@end
Class RCTWebSocketModuleCls(void)
{
return RCTWebSocketModule.class;
}

View File

@@ -0,0 +1,68 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
header_search_paths = [
"\"$(PODS_TARGET_SRCROOT)/React/CoreModules\"",
"\"${PODS_ROOT}/Headers/Public/ReactCodegen/react/renderer/components\"",
]
Pod::Spec.new do |s|
s.name = "React-CoreModules"
s.version = version
s.summary = "-" # TODO
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.compiler_flags = '-Wno-nullability-completeness'
s.source = source
s.source_files = podspec_sources("**/*.{c,m,mm,cpp}", "**/*.h")
s.ios.exclude_files = "PlatformStubs/**/*"
exclude_files = ["RCTStatusBarManager.mm"]
s.macos.exclude_files = exclude_files
s.visionos.exclude_files = exclude_files
s.tvos.exclude_files = exclude_files
s.header_dir = "CoreModules"
s.pod_target_xcconfig = {
"USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"HEADER_SEARCH_PATHS" => header_search_paths.join(" ")
}
s.framework = "UIKit"
s.dependency "RCTTypeSafety", version
s.dependency "React-Core/CoreModulesHeaders", version
s.dependency "React-RCTImage", version
s.dependency "React-jsi", version
s.dependency 'React-RCTBlob'
add_dependency(s, "React-debug")
add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
add_dependency(s, "React-RCTFBReactNativeSpec")
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
add_dependency(s, "React-NativeModulesApple")
add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"])
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
end