17f6367d0SBen Roth// Copyright 2015-present 650 Industries. All rights reserved. 27f6367d0SBen Roth 37f6367d0SBen Roth@import UIKit; 47f6367d0SBen Roth 57f6367d0SBen Roth#import "EXAppDelegate.h" 67f6367d0SBen Roth#import "EXAppViewController.h" 7b0932293SBen Roth#import "EXButtonView.h" 87f6367d0SBen Roth#import "EXHomeAppManager.h" 97f6367d0SBen Roth#import "EXHomeDiagnosticsViewController.h" 107f6367d0SBen Roth#import "EXKernel.h" 119088dd0eSBen Roth#import "EXAppLoader.h" 127f6367d0SBen Roth#import "EXKernelAppRecord.h" 137f6367d0SBen Roth#import "EXKernelAppRegistry.h" 14b0932293SBen Roth#import "EXKernelDevKeyCommands.h" 15f322f621SBen Roth#import "EXKernelLinkingManager.h" 167f6367d0SBen Roth#import "EXKernelServiceRegistry.h" 174e9980b6SBen Roth#import "EXMenuGestureRecognizer.h" 187f6367d0SBen Roth#import "EXMenuViewController.h" 197f6367d0SBen Roth#import "EXRootViewController.h" 203980c593STomasz Sapeta#import "EXMenuWindow.h" 217f6367d0SBen Roth 2266169681SBen RothNSString * const kEXHomeDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey"; 2366169681SBen RothNSString * const kEXHomeIsNuxFinishedDefaultsKey = @"EXHomeIsNuxFinishedDefaultsKey"; 2466169681SBen Roth 257f6367d0SBen RothNS_ASSUME_NONNULL_BEGIN 267f6367d0SBen Roth 27b987b50fSBen Roth@interface EXRootViewController () <EXAppBrowserController> 287f6367d0SBen Roth 297f6367d0SBen Roth@property (nonatomic, strong) EXMenuViewController *menuViewController; 307f6367d0SBen Roth@property (nonatomic, assign) BOOL isMenuVisible; 31edc8619eSBen Roth@property (nonatomic, assign) BOOL isAnimatingMenu; 32edc8619eSBen Roth@property (nonatomic, assign) BOOL isAnimatingAppTransition; 33b0932293SBen Roth@property (nonatomic, strong) EXButtonView *btnMenu; 34d718b4b8SBen Roth@property (nonatomic, strong, nullable) EXMenuWindow *menuWindow; 357f6367d0SBen Roth 367f6367d0SBen Roth@end 377f6367d0SBen Roth 387f6367d0SBen Roth@implementation EXRootViewController 397f6367d0SBen Roth 407f6367d0SBen Roth- (instancetype)init 417f6367d0SBen Roth{ 427f6367d0SBen Roth if (self = [super init]) { 437f6367d0SBen Roth [EXKernel sharedInstance].browserController = self; 44b0932293SBen Roth [[NSNotificationCenter defaultCenter] addObserver:self 454e9980b6SBen Roth selector:@selector(_updateMenuButtonBehavior) 46b0932293SBen Roth name:kEXKernelDidChangeMenuBehaviorNotification 47b0932293SBen Roth object:nil]; 4889fd6b24SBen Roth [self _maybeResetNuxState]; 497f6367d0SBen Roth } 507f6367d0SBen Roth return self; 517f6367d0SBen Roth} 527f6367d0SBen Roth 53b0932293SBen Roth- (void)viewDidLoad 54b0932293SBen Roth{ 55b0932293SBen Roth [super viewDidLoad]; 56b0932293SBen Roth _btnMenu = [[EXButtonView alloc] init]; 57b0932293SBen Roth _btnMenu.hidden = YES; 58b0932293SBen Roth [self.view addSubview:_btnMenu]; 594e9980b6SBen Roth EXMenuGestureRecognizer *menuGestureRecognizer = [[EXMenuGestureRecognizer alloc] initWithTarget:self action:@selector(_onMenuGestureRecognized:)]; 604e9980b6SBen Roth [((EXAppDelegate *)[UIApplication sharedApplication].delegate).window addGestureRecognizer:menuGestureRecognizer]; 61b0932293SBen Roth} 62b0932293SBen Roth 63b0932293SBen Roth- (void)viewWillLayoutSubviews 64b0932293SBen Roth{ 65b0932293SBen Roth [super viewWillLayoutSubviews]; 66b0932293SBen Roth _btnMenu.frame = CGRectMake(0, 0, 48.0f, 48.0f); 67b0932293SBen Roth _btnMenu.center = CGPointMake(self.view.frame.size.width - 36.0f, self.view.frame.size.height - 72.0f); 68b0932293SBen Roth [self.view bringSubviewToFront:_btnMenu]; 69b0932293SBen Roth} 70b0932293SBen Roth 7189fd6b24SBen Roth#pragma mark - EXViewController 727f6367d0SBen Roth 7389fd6b24SBen Roth- (void)createRootAppAndMakeVisible 7489fd6b24SBen Roth{ 757f6367d0SBen Roth EXHomeAppManager *homeAppManager = [[EXHomeAppManager alloc] init]; 769088dd0eSBen Roth EXAppLoader *homeAppLoader = [[EXAppLoader alloc] initWithLocalManifest:[EXHomeAppManager bundledHomeManifest]]; 777f6367d0SBen Roth EXKernelAppRecord *homeAppRecord = [[EXKernelAppRecord alloc] initWithAppLoader:homeAppLoader appManager:homeAppManager]; 787f6367d0SBen Roth [[EXKernel sharedInstance].appRegistry registerHomeAppRecord:homeAppRecord]; 797f6367d0SBen Roth [self moveAppToVisible:homeAppRecord]; 807f6367d0SBen Roth} 817f6367d0SBen Roth 827f6367d0SBen Roth#pragma mark - EXAppBrowserController 837f6367d0SBen Roth 847f6367d0SBen Roth- (void)moveAppToVisible:(EXKernelAppRecord *)appRecord 857f6367d0SBen Roth{ 867f6367d0SBen Roth [self _foregroundAppRecord:appRecord]; 87*bbef49b2SBrent Vatne 88*bbef49b2SBrent Vatne // When foregrounding the app record we want to add it to the history to handle the edge case 89*bbef49b2SBrent Vatne // where a user opened a project, then went to home and cleared history, then went back to a 90*bbef49b2SBrent Vatne // the already open project. 91*bbef49b2SBrent Vatne [self addHistoryItemWithUrl:appRecord.appLoader.manifestUrl manifest:appRecord.appLoader.manifest]; 927f6367d0SBen Roth} 937f6367d0SBen Roth 94edc8619eSBen Roth- (void)toggleMenuWithCompletion:(void (^ _Nullable)(void))completion 957f6367d0SBen Roth{ 96edc8619eSBen Roth [self setIsMenuVisible:!_isMenuVisible completion:completion]; 977f6367d0SBen Roth} 987f6367d0SBen Roth 99edc8619eSBen Roth- (void)setIsMenuVisible:(BOOL)isMenuVisible completion:(void (^ _Nullable)(void))completion 1007f6367d0SBen Roth{ 1017f6367d0SBen Roth if (!_menuViewController) { 1027f6367d0SBen Roth _menuViewController = [[EXMenuViewController alloc] init]; 1037f6367d0SBen Roth } 104bddeb74fSBen Roth if (isMenuVisible != _isMenuVisible) { 105bddeb74fSBen Roth if (!_isAnimatingMenu) { 1067f6367d0SBen Roth _isMenuVisible = isMenuVisible; 107edc8619eSBen Roth [self _animateMenuToVisible:_isMenuVisible completion:completion]; 1087f6367d0SBen Roth } 109bddeb74fSBen Roth } else { 110bddeb74fSBen Roth completion(); 111bddeb74fSBen Roth } 1127f6367d0SBen Roth} 1137f6367d0SBen Roth 114b987b50fSBen Roth- (void)showDiagnostics 115b987b50fSBen Roth{ 116edc8619eSBen Roth __weak typeof(self) weakSelf = self; 117edc8619eSBen Roth [self setIsMenuVisible:NO completion:^{ 118edc8619eSBen Roth __strong typeof(weakSelf) strongSelf = weakSelf; 119edc8619eSBen Roth if (strongSelf) { 120b987b50fSBen Roth EXHomeDiagnosticsViewController *vcDiagnostics = [[EXHomeDiagnosticsViewController alloc] init]; 121edc8619eSBen Roth [strongSelf presentViewController:vcDiagnostics animated:NO completion:nil]; 122edc8619eSBen Roth } 123edc8619eSBen Roth }]; 124b987b50fSBen Roth} 125b987b50fSBen Roth 1262fcba380SBen Roth- (void)showQRReader 1272fcba380SBen Roth{ 1282fcba380SBen Roth [self moveHomeToVisible]; 129f44e163eSBen Roth [[self _getHomeAppManager] showQRReader]; 130b987b50fSBen Roth} 131b987b50fSBen Roth 132b987b50fSBen Roth- (void)moveHomeToVisible 133b987b50fSBen Roth{ 134edc8619eSBen Roth __weak typeof(self) weakSelf = self; 135edc8619eSBen Roth [self setIsMenuVisible:NO completion:^{ 136edc8619eSBen Roth __strong typeof(weakSelf) strongSelf = weakSelf; 137edc8619eSBen Roth if (strongSelf) { 138edc8619eSBen Roth [strongSelf moveAppToVisible:[EXKernel sharedInstance].appRegistry.homeAppRecord]; 1393980c593STomasz Sapeta 1403980c593STomasz Sapeta if (strongSelf.isMenuVisible) { 1413980c593STomasz Sapeta [strongSelf setIsMenuVisible:NO completion:nil]; 1423980c593STomasz Sapeta } 143edc8619eSBen Roth } 144edc8619eSBen Roth }]; 145b987b50fSBen Roth} 146b987b50fSBen Roth 147b987b50fSBen Roth- (void)refreshVisibleApp 148b987b50fSBen Roth{ 149928bc3baSBen Roth // this is different from Util.reload() 150928bc3baSBen Roth // because it can work even on an errored app record (e.g. with no manifest, or with no running bridge). 151edc8619eSBen Roth [self setIsMenuVisible:NO completion:nil]; 15285f025b0SBen Roth EXKernelAppRecord *visibleApp = [EXKernel sharedInstance].visibleApp; 15385f025b0SBen Roth [[EXKernel sharedInstance] logAnalyticsEvent:@"RELOAD_EXPERIENCE" forAppRecord:visibleApp]; 15485f025b0SBen Roth NSURL *urlToRefresh = visibleApp.appLoader.manifestUrl; 1553b6a24fbSTomasz Sapeta 1563b6a24fbSTomasz Sapeta // Unregister visible app record so all modules get destroyed. 1573b6a24fbSTomasz Sapeta [[[EXKernel sharedInstance] appRegistry] unregisterAppWithRecord:visibleApp]; 1583b6a24fbSTomasz Sapeta 1593b6a24fbSTomasz Sapeta // Create new app record. 160b987b50fSBen Roth [[EXKernel sharedInstance] createNewAppWithUrl:urlToRefresh initialProps:nil]; 161b987b50fSBen Roth} 162b987b50fSBen Roth 163f44e163eSBen Roth- (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(NSDictionary *)manifest 164f44e163eSBen Roth{ 165f44e163eSBen Roth [[self _getHomeAppManager] addHistoryItemWithUrl:manifestUrl manifest:manifest]; 166f44e163eSBen Roth} 167f44e163eSBen Roth 168f44e163eSBen Roth- (void)getHistoryUrlForExperienceId:(NSString *)experienceId completion:(void (^)(NSString *))completion 169f44e163eSBen Roth{ 170f44e163eSBen Roth return [[self _getHomeAppManager] getHistoryUrlForExperienceId:experienceId completion:completion]; 171f44e163eSBen Roth} 172f44e163eSBen Roth 17366169681SBen Roth- (void)setIsNuxFinished:(BOOL)isFinished 17466169681SBen Roth{ 17566169681SBen Roth [[NSUserDefaults standardUserDefaults] setBool:isFinished forKey:kEXHomeIsNuxFinishedDefaultsKey]; 17666169681SBen Roth [[NSUserDefaults standardUserDefaults] synchronize]; 17766169681SBen Roth} 17866169681SBen Roth 17966169681SBen Roth- (BOOL)isNuxFinished 18066169681SBen Roth{ 18166169681SBen Roth return [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeIsNuxFinishedDefaultsKey]; 18266169681SBen Roth} 18366169681SBen Roth 18466169681SBen Roth- (void)appDidFinishLoadingSuccessfully:(EXKernelAppRecord *)appRecord 18566169681SBen Roth{ 18666169681SBen Roth // show nux if needed 18766169681SBen Roth if (!self.isNuxFinished 18866169681SBen Roth && appRecord == [EXKernel sharedInstance].visibleApp 18966169681SBen Roth && appRecord != [EXKernel sharedInstance].appRegistry.homeAppRecord 19066169681SBen Roth && !self.isMenuVisible) { 191edc8619eSBen Roth [self setIsMenuVisible:YES completion:nil]; 19266169681SBen Roth } 193b0932293SBen Roth 1944e9980b6SBen Roth // check button availability when any new app loads 1954e9980b6SBen Roth [self _updateMenuButtonBehavior]; 19666169681SBen Roth} 19766169681SBen Roth 198b987b50fSBen Roth#pragma mark - internal 199b987b50fSBen Roth 2007f6367d0SBen Roth- (void)_foregroundAppRecord:(EXKernelAppRecord *)appRecord 2017f6367d0SBen Roth{ 202edc8619eSBen Roth if (_isAnimatingAppTransition) { 203edc8619eSBen Roth return; 2047f6367d0SBen Roth } 205edc8619eSBen Roth EXAppViewController *viewControllerToShow = appRecord.viewController; 206edc8619eSBen Roth EXAppViewController *viewControllerToHide; 207edc8619eSBen Roth if (viewControllerToShow != self.contentViewController) { 208edc8619eSBen Roth _isAnimatingAppTransition = YES; 209edc8619eSBen Roth if (self.contentViewController) { 210edc8619eSBen Roth viewControllerToHide = (EXAppViewController *)self.contentViewController; 211edc8619eSBen Roth } 2127f6367d0SBen Roth if (viewControllerToShow) { 2137f6367d0SBen Roth [viewControllerToShow willMoveToParentViewController:self]; 2147f6367d0SBen Roth [self.view addSubview:viewControllerToShow.view]; 215ce63205bSTomasz Sapeta [viewControllerToShow foregroundControllers]; 2167f6367d0SBen Roth } 2177f6367d0SBen Roth 218edc8619eSBen Roth __weak typeof(self) weakSelf = self; 219edc8619eSBen Roth void (^transitionFinished)(void) = ^{ 220edc8619eSBen Roth __strong typeof(weakSelf) strongSelf = weakSelf; 221edc8619eSBen Roth if (strongSelf) { 222edc8619eSBen Roth if (viewControllerToHide) { 223ce63205bSTomasz Sapeta // backgrounds and then dismisses all modals that are presented by the app 224ce63205bSTomasz Sapeta [viewControllerToHide backgroundControllers]; 225d57b5fd1SBrent Vatne [viewControllerToHide dismissViewControllerAnimated:NO completion:nil]; 226edc8619eSBen Roth [viewControllerToHide willMoveToParentViewController:nil]; 227edc8619eSBen Roth [viewControllerToHide.view removeFromSuperview]; 228edc8619eSBen Roth [viewControllerToHide didMoveToParentViewController:nil]; 229e7ac6e4fSBen Roth } 230edc8619eSBen Roth if (viewControllerToShow) { 231edc8619eSBen Roth [viewControllerToShow didMoveToParentViewController:strongSelf]; 232edc8619eSBen Roth strongSelf.contentViewController = viewControllerToShow; 233edc8619eSBen Roth } 234edc8619eSBen Roth [strongSelf.view setNeedsLayout]; 235edc8619eSBen Roth strongSelf.isAnimatingAppTransition = NO; 236edc8619eSBen Roth if (strongSelf.delegate) { 237edc8619eSBen Roth [strongSelf.delegate viewController:strongSelf didNavigateAppToVisible:appRecord]; 238edc8619eSBen Roth } 239edc8619eSBen Roth } 240edc8619eSBen Roth }; 241edc8619eSBen Roth 242edc8619eSBen Roth BOOL animated = (viewControllerToHide && viewControllerToShow); 243edc8619eSBen Roth if (animated) { 244edc8619eSBen Roth if (viewControllerToHide.contentView) { 245edc8619eSBen Roth viewControllerToHide.contentView.transform = CGAffineTransformIdentity; 246edc8619eSBen Roth viewControllerToHide.contentView.alpha = 1.0f; 247edc8619eSBen Roth } 248edc8619eSBen Roth if (viewControllerToShow.contentView) { 249edc8619eSBen Roth viewControllerToShow.contentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 250edc8619eSBen Roth viewControllerToShow.contentView.alpha = 0; 251edc8619eSBen Roth } 252edc8619eSBen Roth [UIView animateWithDuration:0.3f animations:^{ 253edc8619eSBen Roth if (viewControllerToHide.contentView) { 254edc8619eSBen Roth viewControllerToHide.contentView.transform = CGAffineTransformMakeScale(0.95f, 0.95f); 255edc8619eSBen Roth viewControllerToHide.contentView.alpha = 0.5f; 256edc8619eSBen Roth } 257edc8619eSBen Roth if (viewControllerToShow.contentView) { 258edc8619eSBen Roth viewControllerToShow.contentView.transform = CGAffineTransformIdentity; 259edc8619eSBen Roth viewControllerToShow.contentView.alpha = 1.0f; 260edc8619eSBen Roth } 261edc8619eSBen Roth } completion:^(BOOL finished) { 262edc8619eSBen Roth transitionFinished(); 263edc8619eSBen Roth }]; 264edc8619eSBen Roth } else { 265edc8619eSBen Roth transitionFinished(); 266edc8619eSBen Roth } 267edc8619eSBen Roth } 268edc8619eSBen Roth} 269edc8619eSBen Roth 270edc8619eSBen Roth- (void)_animateMenuToVisible:(BOOL)visible completion:(void (^ _Nullable)(void))completion 271edc8619eSBen Roth{ 272edc8619eSBen Roth _isAnimatingMenu = YES; 273edc8619eSBen Roth __weak typeof(self) weakSelf = self; 274edc8619eSBen Roth if (visible) { 275edc8619eSBen Roth [_menuViewController willMoveToParentViewController:self]; 2763980c593STomasz Sapeta 2773980c593STomasz Sapeta if (_menuWindow == nil) { 2783980c593STomasz Sapeta _menuWindow = [[EXMenuWindow alloc] init]; 2793980c593STomasz Sapeta } 2803980c593STomasz Sapeta 2813980c593STomasz Sapeta [_menuWindow setFrame:self.view.frame]; 2823980c593STomasz Sapeta [_menuWindow addSubview:_menuViewController.view]; 2833980c593STomasz Sapeta [_menuWindow makeKeyAndVisible]; 2843980c593STomasz Sapeta 285edc8619eSBen Roth _menuViewController.view.alpha = 0.0f; 286edc8619eSBen Roth _menuViewController.view.transform = CGAffineTransformMakeScale(1.1f, 1.1f); 287edc8619eSBen Roth [UIView animateWithDuration:0.1f animations:^{ 288d718b4b8SBen Roth self.menuViewController.view.alpha = 1.0f; 289d718b4b8SBen Roth self.menuViewController.view.transform = CGAffineTransformIdentity; 290edc8619eSBen Roth } completion:^(BOOL finished) { 291edc8619eSBen Roth __strong typeof(weakSelf) strongSelf = weakSelf; 292edc8619eSBen Roth if (strongSelf) { 293edc8619eSBen Roth strongSelf.isAnimatingMenu = NO; 294edc8619eSBen Roth [strongSelf.menuViewController didMoveToParentViewController:self]; 295edc8619eSBen Roth if (completion) { 296edc8619eSBen Roth completion(); 297edc8619eSBen Roth } 298edc8619eSBen Roth } 299edc8619eSBen Roth }]; 300edc8619eSBen Roth } else { 301edc8619eSBen Roth _menuViewController.view.alpha = 1.0f; 302edc8619eSBen Roth [UIView animateWithDuration:0.1f animations:^{ 303d718b4b8SBen Roth self.menuViewController.view.alpha = 0.0f; 304edc8619eSBen Roth } completion:^(BOOL finished) { 305edc8619eSBen Roth __strong typeof(weakSelf) strongSelf = weakSelf; 306edc8619eSBen Roth if (strongSelf) { 307edc8619eSBen Roth strongSelf.isAnimatingMenu = NO; 308edc8619eSBen Roth [strongSelf.menuViewController willMoveToParentViewController:nil]; 309edc8619eSBen Roth [strongSelf.menuViewController.view removeFromSuperview]; 310edc8619eSBen Roth [strongSelf.menuViewController didMoveToParentViewController:nil]; 311d718b4b8SBen Roth strongSelf.menuWindow = nil; 312edc8619eSBen Roth if (completion) { 313edc8619eSBen Roth completion(); 314edc8619eSBen Roth } 315edc8619eSBen Roth } 316edc8619eSBen Roth }]; 3177f6367d0SBen Roth } 3187f6367d0SBen Roth} 3197f6367d0SBen Roth 320f44e163eSBen Roth- (EXHomeAppManager *)_getHomeAppManager 321f44e163eSBen Roth{ 322f44e163eSBen Roth return (EXHomeAppManager *)[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager; 323f44e163eSBen Roth} 324f44e163eSBen Roth 32566169681SBen Roth- (void)_maybeResetNuxState 32666169681SBen Roth{ 32766169681SBen Roth // used by appetize: optionally disable nux 32866169681SBen Roth BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:kEXHomeDisableNuxDefaultsKey]; 32966169681SBen Roth if (disableNuxDefaultsValue) { 33066169681SBen Roth [self setIsNuxFinished:YES]; 33166169681SBen Roth [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXHomeDisableNuxDefaultsKey]; 33266169681SBen Roth } 33366169681SBen Roth} 33466169681SBen Roth 3354e9980b6SBen Roth- (void)_updateMenuButtonBehavior 336b0932293SBen Roth{ 3374e9980b6SBen Roth BOOL shouldShowButton = [[EXKernelDevKeyCommands sharedInstance] isLegacyMenuButtonAvailable]; 338b0932293SBen Roth dispatch_async(dispatch_get_main_queue(), ^{ 339d718b4b8SBen Roth self.btnMenu.hidden = !shouldShowButton; 340b0932293SBen Roth }); 341b0932293SBen Roth} 342b0932293SBen Roth 3434e9980b6SBen Roth- (void)_onMenuGestureRecognized:(EXMenuGestureRecognizer *)sender 3444e9980b6SBen Roth{ 3454e9980b6SBen Roth if (sender.state == UIGestureRecognizerStateEnded) { 3464e9980b6SBen Roth [[EXKernel sharedInstance] switchTasks]; 3474e9980b6SBen Roth } 3484e9980b6SBen Roth} 3494e9980b6SBen Roth 3507f6367d0SBen Roth@end 3517f6367d0SBen Roth 3527f6367d0SBen RothNS_ASSUME_NONNULL_END 353