1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import <React/RCTRootView.h>
4
5#import "EXDevMenuViewController.h"
6#import "EXDevMenuManager.h"
7#import "EXKernel.h"
8#import "EXAppLoader.h"
9#import "EXKernelAppRegistry.h"
10#import "EXUtil.h"
11
12@interface EXDevMenuViewController ()
13
14@property (nonatomic, strong) RCTRootView *reactRootView;
15@property (nonatomic, assign) BOOL hasCalledJSLoadedNotification;
16
17@end
18
19@interface RCTRootView (EXDevMenuView)
20
21- (void)javaScriptDidLoad:(NSNotification *)notification;
22- (void)hideLoadingView;
23
24@end
25
26@implementation EXDevMenuViewController
27
28# pragma mark - UIViewController
29
30- (void)viewDidLoad
31{
32  [super viewDidLoad];
33
34  [self _maybeRebuildRootView];
35  [self.view addSubview:_reactRootView];
36}
37
38- (UIRectEdge)edgesForExtendedLayout
39{
40  return UIRectEdgeNone;
41}
42
43- (BOOL)extendedLayoutIncludesOpaqueBars
44{
45  return YES;
46}
47
48- (void)viewWillLayoutSubviews
49{
50  [super viewWillLayoutSubviews];
51  _reactRootView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
52}
53
54- (void)viewWillAppear:(BOOL)animated
55{
56  [super viewWillAppear:animated];
57  [self _maybeRebuildRootView];
58  [self _forceRootViewToRenderHack];
59  [_reactRootView becomeFirstResponder];
60}
61
62- (BOOL)shouldAutorotate
63{
64  return YES;
65}
66
67/**
68 * Overrides UIViewController's method that returns interface orientations that the view controller supports.
69 * If EXDevMenuViewController is currently shown we want to use its supported orientations so the UI rotates
70 * when we open the dev menu while in the unsupported orientation.
71 * Otherwise, returns interface orientations supported by the current experience.
72 */
73- (UIInterfaceOrientationMask)supportedInterfaceOrientations
74{
75  return UIInterfaceOrientationMaskPortrait;
76}
77
78/**
79 * Same case as above with `supportedInterfaceOrientations` method.
80 * If we don't override this, we can get incorrect orientation while changing device orientation when the dev menu is visible.
81 */
82- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
83{
84  return UIInterfaceOrientationPortrait;
85}
86
87#pragma mark - API
88
89
90
91#pragma mark - internal
92
93- (NSDictionary *)_getInitialPropsForVisibleApp
94{
95  EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp;
96  NSDictionary *task = @{
97    @"manifestUrl": visibleApp.appLoader.manifestUrl.absoluteString,
98    @"manifest": (visibleApp.appLoader.manifest) ? visibleApp.appLoader.manifest : [NSNull null],
99  };
100
101  return @{
102    @"task": task,
103    @"uuid": [[NSUUID UUID] UUIDString], // include randomness to force the component to rerender
104  };
105}
106
107// RCTRootView assumes it is created on a loading bridge.
108// in our case, the bridge has usually already loaded. so we need to prod the view.
109- (void)_forceRootViewToRenderHack
110{
111  if (!_hasCalledJSLoadedNotification) {
112    RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge];
113    NSNotification *notif = [[NSNotification alloc] initWithName:RCTJavaScriptDidLoadNotification
114                                                          object:nil
115                                                        userInfo:@{ @"bridge": mainBridge }];
116    [_reactRootView javaScriptDidLoad:notif];
117    _hasCalledJSLoadedNotification = YES;
118  }
119}
120
121- (void)_maybeRebuildRootView
122{
123  RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge];
124
125  // Main bridge might change if the home bridge restarted for some reason (e.g. due to an error)
126  if (!_reactRootView || _reactRootView.bridge != mainBridge) {
127    if (_reactRootView) {
128      [_reactRootView removeFromSuperview];
129      _reactRootView = nil;
130    }
131    _hasCalledJSLoadedNotification = NO;
132
133    _reactRootView = [[RCTRootView alloc] initWithBridge:mainBridge moduleName:@"HomeMenu" initialProperties:[self _getInitialPropsForVisibleApp]];
134    _reactRootView.frame = self.view.bounds;
135
136    // By default react root view has white background,
137    // however devmenu's bottom sheet looks better with partially visible experience.
138    _reactRootView.backgroundColor = [UIColor clearColor];
139
140    if ([self isViewLoaded]) {
141      [self.view addSubview:_reactRootView];
142      [self.view setNeedsLayout];
143    }
144  } else if (_reactRootView) {
145    _reactRootView.appProperties = [self _getInitialPropsForVisibleApp];
146  }
147}
148
149@end
150