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 "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  EXManifestsManifest *manifest = visibleApp.appLoader.manifest;
99  if (manifest && [NSJSONSerialization isValidJSONObject:manifest.rawManifestJSON]) {
100    NSError *error;
101    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:manifest.rawManifestJSON options:0 error:&error];
102    if (jsonData) {
103      manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
104    } else {
105      EXLogWarn(@"Failed to serialize JSON manifest: %@", error);
106    }
107  }
108  NSDictionary *task = @{
109    @"manifestUrl": visibleApp.appLoader.manifestUrl.absoluteString,
110    @"manifestString": manifestString ?: [NSNull null],
111  };
112
113  return @{
114    @"task": task,
115    @"uuid": [[NSUUID UUID] UUIDString], // include randomness to force the component to rerender
116  };
117}
118
119// RCTRootView assumes it is created on a loading bridge.
120// in our case, the bridge has usually already loaded. so we need to prod the view.
121- (void)_forceRootViewToRenderHack
122{
123  if (!_hasCalledJSLoadedNotification) {
124    RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge];
125    NSNotification *notif = [[NSNotification alloc] initWithName:RCTJavaScriptDidLoadNotification
126                                                          object:nil
127                                                        userInfo:@{ @"bridge": mainBridge }];
128    [_reactRootView javaScriptDidLoad:notif];
129    _hasCalledJSLoadedNotification = YES;
130  }
131}
132
133- (void)_maybeRebuildRootView
134{
135  RCTBridge *mainBridge = [[EXDevMenuManager sharedInstance] mainBridge];
136
137  // Main bridge might change if the home bridge restarted for some reason (e.g. due to an error)
138  if (!_reactRootView || _reactRootView.bridge != mainBridge) {
139    if (_reactRootView) {
140      [_reactRootView removeFromSuperview];
141      _reactRootView = nil;
142    }
143    _hasCalledJSLoadedNotification = NO;
144
145    _reactRootView = [[RCTRootView alloc] initWithBridge:mainBridge moduleName:@"HomeMenu" initialProperties:[self _getInitialPropsForVisibleApp]];
146    _reactRootView.frame = self.view.bounds;
147
148    // By default react root view has white background,
149    // however devmenu's bottom sheet looks better with partially visible experience.
150    _reactRootView.backgroundColor = [UIColor clearColor];
151
152    if ([self isViewLoaded]) {
153      [self.view addSubview:_reactRootView];
154      [self.view setNeedsLayout];
155    }
156  } else if (_reactRootView) {
157    _reactRootView.appProperties = [self _getInitialPropsForVisibleApp];
158  }
159}
160
161@end
162