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