diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index f0dbb32bad1b..b216df078262 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -17,454 +17,11 @@ #import #import -#import - #import "CoreModulesPlugins.h" +#import "RCTRedBoxController+Internal.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 - -- (void)redBoxController:(RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame; -- (void)reloadFromRedBoxController:(RCTRedBoxController *)redBoxController; -- (void)loadExtraDataViewController; - -@end - -@interface RCTRedBoxController : UIViewController -@property (nonatomic, weak) id actionDelegate; -@end - -@implementation RCTRedBoxController { - UITableView *_stackTraceTableView; - NSString *_lastErrorMessage; - NSArray *_lastStackTrace; - NSArray *_customButtonTitles; - NSArray *_customButtonHandlers; - int _lastErrorCookie; -} - -- (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTitles - customButtonHandlers:(NSArray *)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]; -#if !TARGET_OS_TV - _stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3]; - _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; -#endif - _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 *)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]]; - } - } -#if !TARGET_OS_TV - UIPasteboard *pb = [UIPasteboard generalPasteboard]; - [pb setString:fullStackTrace]; -#endif -} - -- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame -{ - NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @""; - 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 *)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, diff --git a/packages/react-native/React/CoreModules/RCTRedBoxController+Internal.h b/packages/react-native/React/CoreModules/RCTRedBoxController+Internal.h new file mode 100644 index 000000000000..c8e333c49dea --- /dev/null +++ b/packages/react-native/React/CoreModules/RCTRedBoxController+Internal.h @@ -0,0 +1,41 @@ +/* + * 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 + +#import "RCTRedBox.h" + +#if RCT_DEV_MENU + +@class RCTJSStackFrame; +@class RCTRedBoxController; + +@protocol RCTRedBoxControllerActionDelegate + +- (void)redBoxController:(RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame; +- (void)reloadFromRedBoxController:(RCTRedBoxController *)redBoxController; +- (void)loadExtraDataViewController; + +@end + +@interface RCTRedBoxController : UIViewController + +@property (nonatomic, weak) id actionDelegate; + +- (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTitles + customButtonHandlers:(NSArray *)customButtonHandlers; + +- (void)showErrorMessage:(NSString *)message + withStack:(NSArray *)stack + isUpdate:(BOOL)isUpdate + errorCookie:(int)errorCookie; + +- (void)dismiss; + +@end + +#endif diff --git a/packages/react-native/React/CoreModules/RCTRedBoxController.mm b/packages/react-native/React/CoreModules/RCTRedBoxController.mm new file mode 100644 index 000000000000..8278444f3a6b --- /dev/null +++ b/packages/react-native/React/CoreModules/RCTRedBoxController.mm @@ -0,0 +1,447 @@ +/* + * 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 "RCTRedBoxController+Internal.h" + +#import +#import +#import +#import + +#import + +#if RCT_DEV_MENU + +@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 + +@implementation RCTRedBoxController { + UITableView *_stackTraceTableView; + NSString *_lastErrorMessage; + NSArray *_lastStackTrace; + NSArray *_customButtonTitles; + NSArray *_customButtonHandlers; + int _lastErrorCookie; +} + +- (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTitles + customButtonHandlers:(NSArray *)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]; +#if !TARGET_OS_TV + _stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3]; + _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; +#endif + _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 *)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]]; + } + } +#if !TARGET_OS_TV + UIPasteboard *pb = [UIPasteboard generalPasteboard]; + [pb setString:fullStackTrace]; +#endif +} + +- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame +{ + NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @""; + 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 *)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 + +#endif diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 43ca6dc4f166..52ed16a20766 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<99a7d3e814f4b037ed4496b6eee4f264>> + * @generated SignedSource<<45d368406f020ca101d9b87c7e2527b1>> */ /** @@ -444,6 +444,18 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun preventShadowTreeCommitExhaustion(): Boolean = accessor.preventShadowTreeCommitExhaustion() + /** + * Use the redesigned RedBox error overlay on Android, styled to match the LogBox visual language. + */ + @JvmStatic + public fun redBoxV2Android(): Boolean = accessor.redBoxV2Android() + + /** + * Use the redesigned RedBox error overlay on iOS, styled to match the LogBox visual language. + */ + @JvmStatic + public fun redBoxV2IOS(): Boolean = accessor.redBoxV2IOS() + /** * Function used to enable / disable Pressibility from using W3C Pointer Events for its hover callbacks */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index be22235e4470..1d78c66ea5a3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<5bb0640a99befcfcb3a197a5a074513f>> */ /** @@ -89,6 +89,8 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var perfMonitorV2EnabledCache: Boolean? = null private var preparedTextCacheSizeCache: Double? = null private var preventShadowTreeCommitExhaustionCache: Boolean? = null + private var redBoxV2AndroidCache: Boolean? = null + private var redBoxV2IOSCache: Boolean? = null private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null @@ -731,6 +733,24 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun redBoxV2Android(): Boolean { + var cached = redBoxV2AndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.redBoxV2Android() + redBoxV2AndroidCache = cached + } + return cached + } + + override fun redBoxV2IOS(): Boolean { + var cached = redBoxV2IOSCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.redBoxV2IOS() + redBoxV2IOSCache = cached + } + return cached + } + override fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean { var cached = shouldPressibilityUseW3CPointerEventsForHoverCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 2ba162535ac8..8fa95c07d631 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8667d7237cea82bb5978cb19582d59c0>> + * @generated SignedSource<<23c20167823efb1df8209cdffa7a23d0>> */ /** @@ -166,6 +166,10 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun preventShadowTreeCommitExhaustion(): Boolean + @DoNotStrip @JvmStatic public external fun redBoxV2Android(): Boolean + + @DoNotStrip @JvmStatic public external fun redBoxV2IOS(): Boolean + @DoNotStrip @JvmStatic public external fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean @DoNotStrip @JvmStatic public external fun shouldTriggerResponderTransferOnScrollAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 0bd08cd5665c..fbed6ccb5204 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<17abc72a4045c5695818f254be1783b5>> + * @generated SignedSource<> */ /** @@ -161,6 +161,10 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun preventShadowTreeCommitExhaustion(): Boolean = false + override fun redBoxV2Android(): Boolean = false + + override fun redBoxV2IOS(): Boolean = false + override fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean = false override fun shouldTriggerResponderTransferOnScrollAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index b2e5d18cd8df..5bed35bd2230 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<77ba6c5db120016e6e1f8af195ab3690>> + * @generated SignedSource<<7d3853cb7e319830aab97792e1520ff7>> */ /** @@ -93,6 +93,8 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var perfMonitorV2EnabledCache: Boolean? = null private var preparedTextCacheSizeCache: Double? = null private var preventShadowTreeCommitExhaustionCache: Boolean? = null + private var redBoxV2AndroidCache: Boolean? = null + private var redBoxV2IOSCache: Boolean? = null private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null @@ -804,6 +806,26 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun redBoxV2Android(): Boolean { + var cached = redBoxV2AndroidCache + if (cached == null) { + cached = currentProvider.redBoxV2Android() + accessedFeatureFlags.add("redBoxV2Android") + redBoxV2AndroidCache = cached + } + return cached + } + + override fun redBoxV2IOS(): Boolean { + var cached = redBoxV2IOSCache + if (cached == null) { + cached = currentProvider.redBoxV2IOS() + accessedFeatureFlags.add("redBoxV2IOS") + redBoxV2IOSCache = cached + } + return cached + } + override fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean { var cached = shouldPressibilityUseW3CPointerEventsForHoverCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 41ce962f044a..0d67ebb124f5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8496c138ce5493df84149940df0de944>> + * @generated SignedSource<> */ /** @@ -161,6 +161,10 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun preventShadowTreeCommitExhaustion(): Boolean + @DoNotStrip public fun redBoxV2Android(): Boolean + + @DoNotStrip public fun redBoxV2IOS(): Boolean + @DoNotStrip public fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean @DoNotStrip public fun shouldTriggerResponderTransferOnScrollAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index c176fb1b7631..60f22a831bbb 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5bac13bb6faeffdd3c5eca800f25b96a>> + * @generated SignedSource<> */ /** @@ -453,6 +453,18 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool redBoxV2Android() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("redBoxV2Android"); + return method(javaProvider_); + } + + bool redBoxV2IOS() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("redBoxV2IOS"); + return method(javaProvider_); + } + bool shouldPressibilityUseW3CPointerEventsForHover() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("shouldPressibilityUseW3CPointerEventsForHover"); @@ -922,6 +934,16 @@ bool JReactNativeFeatureFlagsCxxInterop::preventShadowTreeCommitExhaustion( return ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion(); } +bool JReactNativeFeatureFlagsCxxInterop::redBoxV2Android( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::redBoxV2Android(); +} + +bool JReactNativeFeatureFlagsCxxInterop::redBoxV2IOS( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::redBoxV2IOS(); +} + bool JReactNativeFeatureFlagsCxxInterop::shouldPressibilityUseW3CPointerEventsForHover( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::shouldPressibilityUseW3CPointerEventsForHover(); @@ -1260,6 +1282,12 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "preventShadowTreeCommitExhaustion", JReactNativeFeatureFlagsCxxInterop::preventShadowTreeCommitExhaustion), + makeNativeMethod( + "redBoxV2Android", + JReactNativeFeatureFlagsCxxInterop::redBoxV2Android), + makeNativeMethod( + "redBoxV2IOS", + JReactNativeFeatureFlagsCxxInterop::redBoxV2IOS), makeNativeMethod( "shouldPressibilityUseW3CPointerEventsForHover", JReactNativeFeatureFlagsCxxInterop::shouldPressibilityUseW3CPointerEventsForHover), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index d02c0855a993..b1584ffd7ac4 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -237,6 +237,12 @@ class JReactNativeFeatureFlagsCxxInterop static bool preventShadowTreeCommitExhaustion( facebook::jni::alias_ref); + static bool redBoxV2Android( + facebook::jni::alias_ref); + + static bool redBoxV2IOS( + facebook::jni::alias_ref); + static bool shouldPressibilityUseW3CPointerEventsForHover( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 43076de959a4..e7468c18996d 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<211e6d3081b5fc5e5b30e87f04970e95>> */ /** @@ -302,6 +302,14 @@ bool ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion() { return getAccessor().preventShadowTreeCommitExhaustion(); } +bool ReactNativeFeatureFlags::redBoxV2Android() { + return getAccessor().redBoxV2Android(); +} + +bool ReactNativeFeatureFlags::redBoxV2IOS() { + return getAccessor().redBoxV2IOS(); +} + bool ReactNativeFeatureFlags::shouldPressibilityUseW3CPointerEventsForHover() { return getAccessor().shouldPressibilityUseW3CPointerEventsForHover(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 8ba43c099206..5b89b63eea48 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<86b3267ffa68e0f68280957aa54d5041>> + * @generated SignedSource<<4adf6fb186eeb45234b8c1196e9a92c9>> */ /** @@ -384,6 +384,16 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool preventShadowTreeCommitExhaustion(); + /** + * Use the redesigned RedBox error overlay on Android, styled to match the LogBox visual language. + */ + RN_EXPORT static bool redBoxV2Android(); + + /** + * Use the redesigned RedBox error overlay on iOS, styled to match the LogBox visual language. + */ + RN_EXPORT static bool redBoxV2IOS(); + /** * Function used to enable / disable Pressibility from using W3C Pointer Events for its hover callbacks */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 1f40889efc0d..5a23f350c170 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<218ab046c336961b8220c48eb0426b7f>> + * @generated SignedSource<> */ /** @@ -1271,6 +1271,42 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { + auto flagValue = redBoxV2Android_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(69, "redBoxV2Android"); + + flagValue = currentProvider_->redBoxV2Android(); + redBoxV2Android_ = flagValue; + } + + return flagValue.value(); +} + +bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { + auto flagValue = redBoxV2IOS_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(70, "redBoxV2IOS"); + + flagValue = currentProvider_->redBoxV2IOS(); + redBoxV2IOS_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHover() { auto flagValue = shouldPressibilityUseW3CPointerEventsForHover_.load(); @@ -1280,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(71, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1298,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(72, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1316,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(73, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1334,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipToPaddingWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "syncAndroidClipToPaddingWithOverflow"); + markFlagAsAccessed(74, "syncAndroidClipToPaddingWithOverflow"); flagValue = currentProvider_->syncAndroidClipToPaddingWithOverflow(); syncAndroidClipToPaddingWithOverflow_ = flagValue; @@ -1352,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(75, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1370,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(76, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1388,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(77, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1406,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(78, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1424,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "useFabricInterop"); + markFlagAsAccessed(79, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1442,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::useLISAlgorithmInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "useLISAlgorithmInDifferentiator"); + markFlagAsAccessed(80, "useLISAlgorithmInDifferentiator"); flagValue = currentProvider_->useLISAlgorithmInDifferentiator(); useLISAlgorithmInDifferentiator_ = flagValue; @@ -1460,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(81, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1478,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "useNestedScrollViewAndroid"); + markFlagAsAccessed(82, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1496,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useSharedAnimatedBackend"); + markFlagAsAccessed(83, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1514,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(84, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1532,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "useTurboModuleInterop"); + markFlagAsAccessed(85, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1550,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "useTurboModules"); + markFlagAsAccessed(86, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1568,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useUnorderedMapInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useUnorderedMapInDifferentiator"); + markFlagAsAccessed(87, "useUnorderedMapInDifferentiator"); flagValue = currentProvider_->useUnorderedMapInDifferentiator(); useUnorderedMapInDifferentiator_ = flagValue; @@ -1586,7 +1622,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "viewCullingOutsetRatio"); + markFlagAsAccessed(88, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1604,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "viewTransitionEnabled"); + markFlagAsAccessed(89, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1622,7 +1658,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "virtualViewPrerenderRatio"); + markFlagAsAccessed(90, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 7b83ec509d66..73c4cecf8fc8 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<33b0bb5aac0966483d6163484aae10a3>> */ /** @@ -101,6 +101,8 @@ class ReactNativeFeatureFlagsAccessor { bool perfMonitorV2Enabled(); double preparedTextCacheSize(); bool preventShadowTreeCommitExhaustion(); + bool redBoxV2Android(); + bool redBoxV2IOS(); bool shouldPressibilityUseW3CPointerEventsForHover(); bool shouldTriggerResponderTransferOnScrollAndroid(); bool skipActivityIdentityAssertionOnHostPause(); @@ -132,7 +134,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 89> accessedFeatureFlags_; + std::array, 91> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -203,6 +205,8 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> perfMonitorV2Enabled_; std::atomic> preparedTextCacheSize_; std::atomic> preventShadowTreeCommitExhaustion_; + std::atomic> redBoxV2Android_; + std::atomic> redBoxV2IOS_; std::atomic> shouldPressibilityUseW3CPointerEventsForHover_; std::atomic> shouldTriggerResponderTransferOnScrollAndroid_; std::atomic> skipActivityIdentityAssertionOnHostPause_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index b6f431af8f0e..a67085d1e7c2 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<33fd238aafa83c5a42803d3f11d55944>> + * @generated SignedSource<<4ea6e74132f9879a727e4b85e6baa3fc>> */ /** @@ -303,6 +303,14 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool redBoxV2Android() override { + return false; + } + + bool redBoxV2IOS() override { + return false; + } + bool shouldPressibilityUseW3CPointerEventsForHover() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 0d48c7d05074..1c0da695ba04 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2a95ea2091c8e73816acf12daf5e2408>> + * @generated SignedSource<<41290a5bebdecf9c3be32341a79b0317>> */ /** @@ -666,6 +666,24 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::preventShadowTreeCommitExhaustion(); } + bool redBoxV2Android() override { + auto value = values_["redBoxV2Android"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::redBoxV2Android(); + } + + bool redBoxV2IOS() override { + auto value = values_["redBoxV2IOS"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::redBoxV2IOS(); + } + bool shouldPressibilityUseW3CPointerEventsForHover() override { auto value = values_["shouldPressibilityUseW3CPointerEventsForHover"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index a6246efc165a..8943d02cf1ca 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<89c9431802f2653efb73f160b5ac9608>> */ /** @@ -94,6 +94,8 @@ class ReactNativeFeatureFlagsProvider { virtual bool perfMonitorV2Enabled() = 0; virtual double preparedTextCacheSize() = 0; virtual bool preventShadowTreeCommitExhaustion() = 0; + virtual bool redBoxV2Android() = 0; + virtual bool redBoxV2IOS() = 0; virtual bool shouldPressibilityUseW3CPointerEventsForHover() = 0; virtual bool shouldTriggerResponderTransferOnScrollAndroid() = 0; virtual bool skipActivityIdentityAssertionOnHostPause() = 0; diff --git a/packages/react-native/ReactCommon/react/featureflags/rewrite_feature_flag_defaults.py b/packages/react-native/ReactCommon/react/featureflags/rewrite_feature_flag_defaults.py new file mode 100644 index 000000000000..ed146edd4534 --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/rewrite_feature_flag_defaults.py @@ -0,0 +1,79 @@ +#!/usr/bin/env fbpython +# 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. + +# pyre-strict + +"""Rewrite default return values in ReactNativeFeatureFlagsDefaults.h. + +Reads the header from --input, writes the transformed header to stdout. +Overrides are passed as a JSON object via --overrides. +Fails with a non-zero exit code if any requested flag is not found. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys + + +def cxx_literal(value: object) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + s = str(value) + if isinstance(value, int) or "." not in s: + s += ".0" + return s + raise ValueError(f"Unsupported value type {type(value).__name__} for override") + + +def rewrite(source: bytes, overrides: dict[str, object]) -> bytes: + text = source.decode("utf-8") + for name, value in overrides.items(): + cxx_type = "bool" if isinstance(value, bool) else "double" + pattern = rf""" + ( # group 1: everything up to the value + {cxx_type} \s+ # return type + {re.escape(name)} # method name + \s* \( \s* \) # parameter list + \s+ override # override specifier + \s* \{{ # opening brace + [^}}]*? # body before the return (non-greedy, no nested braces) + return \s+ # return keyword + ) + [^;]+ # the value to replace + ( \s* ; ) # group 2: semicolon + """ + text, n = re.subn( + pattern, + rf"\g<1>{cxx_literal(value)}\2", + text, + count=1, + flags=re.DOTALL | re.VERBOSE, + ) + if n != 1: + raise ValueError(f"{name} not matched") + + return text.encode("utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--overrides", default="{}") + parser.add_argument("--input", required=True) + args = parser.parse_args() + + overrides: dict[str, object] = json.loads(args.overrides) + with open(args.input, "rb") as f: + source = f.read() + + sys.stdout.buffer.write(rewrite(source, overrides)) + + +if __name__ == "__main__": + main() diff --git a/packages/react-native/ReactCommon/react/featureflags/tests/test_rewrite_feature_flag_defaults.py b/packages/react-native/ReactCommon/react/featureflags/tests/test_rewrite_feature_flag_defaults.py new file mode 100644 index 000000000000..3483ba3b725e --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/tests/test_rewrite_feature_flag_defaults.py @@ -0,0 +1,60 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# pyre-strict + +from __future__ import annotations + +import os +import unittest + +from rewrite_feature_flag_defaults import cxx_literal, rewrite + + +def _load_header() -> bytes: + with open(os.environ["HEADER_PATH"], "rb") as f: + return f.read() + + +class RewriteFeatureFlagDefaultsTest(unittest.TestCase): + def setUp(self) -> None: + self.source = _load_header() + + def test_empty_overrides_is_passthrough(self) -> None: + self.assertEqual(rewrite(self.source, {}), self.source) + + def test_override_bool_to_true(self) -> None: + result = rewrite(self.source, {"commonTestFlag": True}) + start, end = self._method_body_range(result, "commonTestFlag") + self.assertIn(b"return true;", result[start:end]) + + def test_override_bool_to_false(self) -> None: + result = rewrite(self.source, {"commonTestFlag": False}) + start, end = self._method_body_range(result, "commonTestFlag") + self.assertIn(b"return false;", result[start:end]) + + def test_cxx_literal_int_produces_double(self) -> None: + self.assertEqual(cxx_literal(42), "42.0") + + def test_cxx_literal_float(self) -> None: + self.assertEqual(cxx_literal(3.14), "3.14") + + def test_unmatched_flag_raises(self) -> None: + with self.assertRaises(ValueError): + rewrite(self.source, {"bogusFlag": True}) + + def test_only_target_method_body_changes(self) -> None: + result = rewrite(self.source, {"commonTestFlag": True}) + src_start, src_end = self._method_body_range(self.source, "commonTestFlag") + res_start, res_end = self._method_body_range(result, "commonTestFlag") + self.assertEqual(self.source[:src_start], result[:res_start]) + self.assertEqual(self.source[src_end:], result[res_end:]) + + def _method_body_range(self, source: bytes, name: str) -> tuple[int, int]: + idx = source.find(name.encode()) + self.assertNotEqual(idx, -1, f"{name} not found in output") + open_brace = source.find(b"{", idx) + close_brace = source.find(b"}", open_brace) + return (open_brace, close_brace + 1) diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 2e9a936b9e1e..27265764d993 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3d49e243422f2c220ab36f3e32a78e38>> + * @generated SignedSource<<48e9d8e58caa8fd45c380153714d1166>> */ /** @@ -389,6 +389,16 @@ bool NativeReactNativeFeatureFlags::preventShadowTreeCommitExhaustion( return ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion(); } +bool NativeReactNativeFeatureFlags::redBoxV2Android( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::redBoxV2Android(); +} + +bool NativeReactNativeFeatureFlags::redBoxV2IOS( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::redBoxV2IOS(); +} + bool NativeReactNativeFeatureFlags::shouldPressibilityUseW3CPointerEventsForHover( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::shouldPressibilityUseW3CPointerEventsForHover(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 9580cbc99d25..d7244eb917f8 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3e07a28d13e142ba3c734ca111eb4974>> + * @generated SignedSource<> */ /** @@ -174,6 +174,10 @@ class NativeReactNativeFeatureFlags bool preventShadowTreeCommitExhaustion(jsi::Runtime& runtime); + bool redBoxV2Android(jsi::Runtime& runtime); + + bool redBoxV2IOS(jsi::Runtime& runtime); + bool shouldPressibilityUseW3CPointerEventsForHover(jsi::Runtime& runtime); bool shouldTriggerResponderTransferOnScrollAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 4a1ea7a74db7..853d28924bdd 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -785,6 +785,28 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'experimental', }, + redBoxV2Android: { + defaultValue: false, + metadata: { + dateAdded: '2026-03-25', + description: + 'Use the redesigned RedBox error overlay on Android, styled to match the LogBox visual language.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, + redBoxV2IOS: { + defaultValue: false, + metadata: { + dateAdded: '2026-03-25', + description: + 'Use the redesigned RedBox error overlay on iOS, styled to match the LogBox visual language.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, shouldPressibilityUseW3CPointerEventsForHover: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index c225edc521d6..c4bb98382822 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -116,6 +116,8 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ perfMonitorV2Enabled: Getter, preparedTextCacheSize: Getter, preventShadowTreeCommitExhaustion: Getter, + redBoxV2Android: Getter, + redBoxV2IOS: Getter, shouldPressibilityUseW3CPointerEventsForHover: Getter, shouldTriggerResponderTransferOnScrollAndroid: Getter, skipActivityIdentityAssertionOnHostPause: Getter, @@ -478,6 +480,14 @@ export const preparedTextCacheSize: Getter = createNativeFlagGetter('pre * Enables a new mechanism in ShadowTree to prevent problems caused by multiple threads trying to commit concurrently. If a thread tries to commit a few times unsuccessfully, it will acquire a lock and try again. */ export const preventShadowTreeCommitExhaustion: Getter = createNativeFlagGetter('preventShadowTreeCommitExhaustion', false); +/** + * Use the redesigned RedBox error overlay on Android, styled to match the LogBox visual language. + */ +export const redBoxV2Android: Getter = createNativeFlagGetter('redBoxV2Android', false); +/** + * Use the redesigned RedBox error overlay on iOS, styled to match the LogBox visual language. + */ +export const redBoxV2IOS: Getter = createNativeFlagGetter('redBoxV2IOS', false); /** * Function used to enable / disable Pressibility from using W3C Pointer Events for its hover callbacks */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 42ebd77f42e4..a74e55aec9fc 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0e66e4ae4407000706cd243ad17aa605>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -94,6 +94,8 @@ export interface Spec extends TurboModule { +perfMonitorV2Enabled?: () => boolean; +preparedTextCacheSize?: () => number; +preventShadowTreeCommitExhaustion?: () => boolean; + +redBoxV2Android?: () => boolean; + +redBoxV2IOS?: () => boolean; +shouldPressibilityUseW3CPointerEventsForHover?: () => boolean; +shouldTriggerResponderTransferOnScrollAndroid?: () => boolean; +skipActivityIdentityAssertionOnHostPause?: () => boolean;