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