diff --git a/Q-municate/Categories/QBSettings/QBSettings+Qmunicate.h b/Q-municate/Categories/QBSettings/QBSettings+Qmunicate.h index de94b8325..86a208139 100644 --- a/Q-municate/Categories/QBSettings/QBSettings+Qmunicate.h +++ b/Q-municate/Categories/QBSettings/QBSettings+Qmunicate.h @@ -15,7 +15,7 @@ typedef NS_ENUM(NSUInteger, QMApplicationZone) { QMApplicationZoneQA, }; -static const QMApplicationZone QMCurrentApplicationZone = QMApplicationZoneDevelopment; +static const QMApplicationZone QMCurrentApplicationZone = QMApplicationZoneQA; @interface QBSettings (Qmunicate) diff --git a/Q-municate/Classes/Core/QMCore/QMCore.h b/Q-municate/Classes/Core/QMCore/QMCore.h index b86502181..0aefa06b9 100644 --- a/Q-municate/Classes/Core/QMCore/QMCore.h +++ b/Q-municate/Classes/Core/QMCore/QMCore.h @@ -74,6 +74,16 @@ QMOpenGraphCacheDataSource, QMOpenGraphServiceDelegate> */ @property (copy, nonatomic, nullable) NSString *activeDialogID; +/** + A block called when login process completes with critical authorization error/errors. + + @discussion Common critical situations: + - An user, who signed in via Facebook, has changed facebook password. + - An user, who signed in via Facebook, has removed application from settings. + - There is no authorized user. + */ +@property (copy, nonatomic, nullable) dispatch_block_t athorizationErrorBlock; + /** * QMCore shared instance. * diff --git a/Q-municate/Classes/Core/QMCore/QMCore.m b/Q-municate/Classes/Core/QMCore/QMCore.m index b218a4b57..0b1f4a4a6 100644 --- a/Q-municate/Classes/Core/QMCore/QMCore.m +++ b/Q-municate/Classes/Core/QMCore/QMCore.m @@ -24,11 +24,15 @@ static NSString *const kQMErrorKey = @"errors"; static NSString *const kQMBaseErrorKey = @"base"; +static const NSInteger kQMNotAuthorizedInRest = -1000; +static const NSInteger kQMUnauthorizedErrorCode = -1011; + static NSString *const kQMContactListCacheNameKey = @"q-municate-contacts"; static NSString *const kQMOpenGraphCacheNameKey = @"q-municate-open-graph"; @interface QMCore () +@property (weak, nonatomic) BFTask *loginTask; @property (strong, nonatomic) NSMutableOrderedSet *cachedVocabularyStrings; @end @@ -98,8 +102,13 @@ - (void)configureReachability { @weakify(self); [_internetConnection setReachableBlock:^(Reachability __unused *reachability) { - @strongify(self); dispatch_async(dispatch_get_main_queue(), ^{ + + if (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) { + //No needs to perform login if application is in background state + return; + } + @strongify(self); // reachability block could possibly be called in background thread [self login]; }); @@ -206,10 +215,27 @@ - (void)handleErrorResponse:(QBResponse *)response { - (BFTask *)login { - return [[QMTasks taskAutoLogin] - continueWithSuccessBlock:^id(BFTask *task) { - return [self.chatService connectWithUserID:task.result.ID password:task.result.password]; - }]; + if (self.loginTask) { + return nil; + } + + self.loginTask = + [[QMTasks taskAutoLogin] continueWithBlock:^id _Nullable(BFTask * _Nonnull loginTask) { + + if (loginTask.error) { + if (isCriritalAuthorizationError(loginTask.error) && + self.athorizationErrorBlock) { + self.athorizationErrorBlock(); + self.athorizationErrorBlock = nil; + } + return loginTask; + } + return [self.chatService connectWithUserID:loginTask.result.ID + password:loginTask.result.password]; + }]; + + + return self.loginTask; } - (BFTask *)logout { @@ -432,4 +458,26 @@ - (void)authService:(QMAuthService *)__unused authService } } +static BOOL isCriritalAuthorizationError(NSError *error) { + NSCParameterAssert(error); + NSInteger errorCode = error.code; + if (errorCode == kQMNotAuthorizedInRest + || errorCode == kQMUnauthorizedErrorCode + || isFacebookSessionError(error) + || (errorCode == kBFMultipleErrorsError + && ([error.userInfo[BFTaskMultipleErrorsUserInfoKey][0] code] == kQMUnauthorizedErrorCode + || [error.userInfo[BFTaskMultipleErrorsUserInfoKey][1] code] == kQMUnauthorizedErrorCode))) { + return YES; + } + + return NO; +} + +static BOOL isFacebookSessionError(NSError *error) { + NSString *errorType = + error.userInfo[@"com.facebook.sdk:FBSDKGraphRequestErrorParsedJSONResponseKey"][@"body"][@"error"][@"type"]; + + return [errorType isEqualToString:@"OAuthException"]; +} + @end diff --git a/Q-municate/Classes/QMFacebook/QMFacebook.m b/Q-municate/Classes/QMFacebook/QMFacebook.m index 9e9469300..dfee229a4 100644 --- a/Q-municate/Classes/QMFacebook/QMFacebook.m +++ b/Q-municate/Classes/QMFacebook/QMFacebook.m @@ -18,46 +18,61 @@ static NSString * const kQMAppName = @"Q-municate"; static NSString * const kQMDataKey = @"data"; -static NSString * const kFBGraphGetPictureFormat = @"https://graph.facebook.com/%@/picture?height=100&width=100&access_token=%@"; +static NSString * const kFBGraphGetPictureFormat = +@"https://graph.facebook.com/%@/picture?height=100&width=100&access_token=%@"; + @implementation QMFacebook + (BFTask *)connect { - BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; - FBSDKAccessToken *session = [FBSDKAccessToken currentAccessToken]; if (!session) { + UINavigationController *navigationController = + (id)[[UIApplication sharedApplication].windows.firstObject rootViewController]; - UINavigationController *navigationController = (UINavigationController *)[[UIApplication sharedApplication].windows.firstObject rootViewController]; + NSArray *readPermissions = + @[@"email", @"public_profile", @"user_friends"]; FBSDKLoginManager *loginManager = [[FBSDKLoginManager alloc] init]; - [loginManager logInWithReadPermissions:@[@"email", @"public_profile", @"user_friends"] - fromViewController:navigationController - handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) { - - if (error) { - - [source setError:error]; - } - else if (result.isCancelled) { - - [source cancel]; - - } - else { - - [source setResult:result.token.tokenString]; - } - }]; + + return make_task(^(BFTaskCompletionSource * _Nonnull source) { + + [loginManager logInWithReadPermissions:readPermissions + fromViewController:navigationController + handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) + { + if (error) { + [source setError:error]; + } + else if (result.isCancelled) { + [source cancel]; + } + else { + [source setResult:result.token.tokenString]; + } + }]; + + }); } else { - [source setResult:session.tokenString]; + /* + Handling an Invalidated Session. + + We cannot know if the cached Facebook session is valid until we attempt to make a request to the API. + A session can become invalidated if a user changes their password or revokes the application's privileges. + When this happens, the user needs to be logged out. We can identify an invalid session error within a FBSDKGraphRequest completion + handler. + + If the request fails, we can check if it was due to an invalid session by: + if ([error.userInfo[@"error"][@"type"] isEqualToString: @"OAuthException"]) + */ + return [[self loadMe] continueWithSuccessBlock:^id _Nullable(BFTask * _Nonnull __unused t) { + return [BFTask taskWithResult:session.tokenString]; + }]; } - - return source.task; } + (NSURL *)userImageUrlWithUserID:(NSString *)userID { @@ -70,16 +85,18 @@ + (NSURL *)userImageUrlWithUserID:(NSString *)userID { + (BFTask *)loadMe { - BFTaskCompletionSource* source = [BFTaskCompletionSource taskCompletionSource]; - - FBSDKGraphRequest *friendsRequest = [[FBSDKGraphRequest alloc] initWithGraphPath:@"me" parameters:nil]; - - [friendsRequest startWithCompletionHandler:^(FBSDKGraphRequestConnection *__unused connection, id result, NSError *error) { + return make_task(^(BFTaskCompletionSource * _Nonnull source) { - error != nil ? [source setError:error] : [source setResult:result]; - }]; - - return source.task; + FBSDKGraphRequest *myInfoRequest = + [[FBSDKGraphRequest alloc] initWithGraphPath:@"me" parameters:nil]; + [myInfoRequest setGraphErrorRecoveryDisabled:YES]; + [myInfoRequest startWithCompletionHandler:^(FBSDKGraphRequestConnection *__unused connection, + id result, + NSError *error) + { + error ? [source setError:error] : [source setResult:result]; + }]; + }); } + (void)logout { diff --git a/Q-municate/Classes/QMTasks/QMTasks.m b/Q-municate/Classes/QMTasks/QMTasks.m index de9505510..2e57d69fe 100644 --- a/Q-municate/Classes/QMTasks/QMTasks.m +++ b/Q-municate/Classes/QMTasks/QMTasks.m @@ -116,8 +116,8 @@ + (BFTask *)taskAutoLogin { } else if (type == QMAccountTypeFacebook) { - return [[QMFacebook connect] continueWithBlock:^id(BFTask *task) { - return task.result ? [core.authService loginWithFacebookSessionToken:task.result] : nil; + return [[QMFacebook connect] continueWithSuccessBlock:^id(BFTask *task) { + return [core.authService loginWithFacebookSessionToken:task.result]; }]; } else if (type == QMAccountTypePhone) { @@ -168,10 +168,10 @@ + (BFTask *)taskFetchAllData { [self sliceArray:dialogsUsersIDs.allObjects limit:kQMUsersPageLimit enumerate:^(NSArray *slice, NSRange __unused range) - { - BFTask *> *task = [core.usersService getUsersWithIDs:slice]; - [usersLoadingTasks addObject:task]; - }]; + { + BFTask *> *task = [core.usersService getUsersWithIDs:slice]; + [usersLoadingTasks addObject:task]; + }]; } }; @@ -234,15 +234,15 @@ + (BFTask *)taskUpdateContacts { [self sliceArray:contactsIDs limit:kQMUsersPageLimit enumerate:^(NSArray *slice, NSRange range) - { - QBGeneralResponsePage *page = - [QBGeneralResponsePage responsePageWithCurrentPage:1 - perPage:range.length]; - BFTask *task = - [core.usersService searchUsersWithExtendedRequest:filterForUsersFetch(slice, dateFilter) - page:page]; - [tasks addObject:task]; - }]; + { + QBGeneralResponsePage *page = + [QBGeneralResponsePage responsePageWithCurrentPage:1 + perPage:range.length]; + BFTask *task = + [core.usersService searchUsersWithExtendedRequest:filterForUsersFetch(slice, dateFilter) + page:page]; + [tasks addObject:task]; + }]; BFTask *task = [[BFTask taskForCompletionOfAllTasks:[tasks copy]] continueWithSuccessBlock:^id(BFTask * __unused t) { core.currentProfile.lastUserFetchDate = [NSDate date]; diff --git a/Q-municate/Scenes/Main/QMDialogsViewController/QMDialogsViewController.m b/Q-municate/Scenes/Main/QMDialogsViewController/QMDialogsViewController.m index bdec00678..acb588cba 100644 --- a/Q-municate/Scenes/Main/QMDialogsViewController/QMDialogsViewController.m +++ b/Q-municate/Scenes/Main/QMDialogsViewController/QMDialogsViewController.m @@ -24,9 +24,6 @@ #import "QMNavigationBar.h" #import -static const NSInteger kQMNotAuthorizedInRest = -1000; -static const NSInteger kQMUnauthorizedErrorCode = -1011; - @interface QMDialogsViewController () *> * _Nonnull t) { if (t.result.count > 0) { @@ -162,37 +165,18 @@ - (void)performAutoLoginAndFetchData { QBChat.instance.manualInitialPresence = YES; } - [[[QMCore.instance login] continueWithBlock:^id(BFTask *task) { - + [[QMCore.instance login] continueWithBlock:^id(BFTask *task) { if (task.isFaulted) { - [navigationController dismissNotificationPanel]; - - NSInteger errorCode = task.error.code; - if (errorCode == kQMNotAuthorizedInRest - || errorCode == kQMUnauthorizedErrorCode - || (errorCode == kBFMultipleErrorsError - && ([task.error.userInfo[BFTaskMultipleErrorsUserInfoKey][0] code] == kQMUnauthorizedErrorCode - || [task.error.userInfo[BFTaskMultipleErrorsUserInfoKey][1] code] == kQMUnauthorizedErrorCode))) { - - return [QMCore.instance logout]; - } - } - - if (QMCore.instance.pushNotificationManager.pushNotification != nil) { - [QMCore.instance.pushNotificationManager handlePushNotificationWithDelegate:self]; } - - if (QMCore.instance.currentProfile.pushNotificationsEnabled) { - [QMCore.instance.pushNotificationManager registerAndSubscribeForPushNotifications]; - } - - return [BFTask cancelledTask]; - - }] continueWithBlock:^id _Nullable(BFTask * _Nonnull task) { - - if (!task.isCancelled) { - [self performSegueWithIdentifier:kQMSceneSegueAuth sender:nil]; + else { + if (QMCore.instance.pushNotificationManager.pushNotification != nil) { + [QMCore.instance.pushNotificationManager handlePushNotificationWithDelegate:self]; + } + + if (QMCore.instance.currentProfile.pushNotificationsEnabled) { + [QMCore.instance.pushNotificationManager registerAndSubscribeForPushNotifications]; + } } return nil; @@ -463,7 +447,7 @@ - (void)usersService:(QMUsersService *)__unused usersService if ([self.tableView.dataSource isKindOfClass:[QMDialogsDataSource class]]) { [self.tableView reloadData]; - + } } @@ -552,7 +536,7 @@ - (void)updateDataAndEndRefreshing { [[[QMTasks taskFetchAllData] continueWithBlock:^id _Nullable(BFTask * __unused t) { return [QMTasks taskUpdateContacts]; }] continueWithBlock:^id _Nullable(BFTask * __unused t) { - [self.refreshControl endRefreshing]; + [self.refreshControl endRefreshing]; return nil; }]; diff --git a/QMShareExtension/Common classes/QMShareTasks.m b/QMShareExtension/Common classes/QMShareTasks.m index b5c6df102..83e77b6a9 100644 --- a/QMShareExtension/Common classes/QMShareTasks.m +++ b/QMShareExtension/Common classes/QMShareTasks.m @@ -24,7 +24,7 @@ @implementation QMItemProviderResult - (NSString *)description { NSMutableString *result = [NSMutableString stringWithString:[super description]]; - [result appendFormat:@"Text: %@\n", _text]; + [result appendFormat:@"Text: %@/n", _text]; [result appendFormat:@"Attachment: %@",_attachment]; return result.copy;