diff --git a/CHANGES.rst b/CHANGES.rst index 507513d45..9f4546f7b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +Changes in 0.9.3 (2019-08-) +=============================================== + +Improvements: + * Prompt to accept integration manager policies on use (#2600). + Changes in 0.9.2 (2019-08-08) =============================================== diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 814e0b268..fcd8e851b 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -95,6 +95,17 @@ 32BF995321FA2A1300698084 /* SettingsKeyBackupViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995221FA2A1300698084 /* SettingsKeyBackupViewState.swift */; }; 32BF995521FA2AB700698084 /* SettingsKeyBackupViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995421FA2AB700698084 /* SettingsKeyBackupViewAction.swift */; }; 32BF995721FB07A400698084 /* SettingsKeyBackupTableViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995621FB07A400698084 /* SettingsKeyBackupTableViewSection.swift */; }; + 32DB557522FDADE50016329E /* ServiceTermsModalCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB556922FDADE50016329E /* ServiceTermsModalCoordinatorType.swift */; }; + 32DB557622FDADE50016329E /* ServiceTermsModalCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB556A22FDADE50016329E /* ServiceTermsModalCoordinatorBridgePresenter.swift */; }; + 32DB557722FDADE50016329E /* ServiceTermsModalCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB556B22FDADE50016329E /* ServiceTermsModalCoordinator.swift */; }; + 32DB557822FDADE50016329E /* ServiceTermsModalScreenViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB556D22FDADE50016329E /* ServiceTermsModalScreenViewState.swift */; }; + 32DB557922FDADE50016329E /* ServiceTermsModalScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB556E22FDADE50016329E /* ServiceTermsModalScreenViewModel.swift */; }; + 32DB557A22FDADE50016329E /* ServiceTermsModalScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB556F22FDADE50016329E /* ServiceTermsModalScreenViewController.swift */; }; + 32DB557B22FDADE50016329E /* ServiceTermsModalScreenViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32DB557022FDADE50016329E /* ServiceTermsModalScreenViewController.storyboard */; }; + 32DB557C22FDADE50016329E /* ServiceTermsModalScreenViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB557122FDADE50016329E /* ServiceTermsModalScreenViewModelType.swift */; }; + 32DB557D22FDADE50016329E /* ServiceTermsModalScreenCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB557222FDADE50016329E /* ServiceTermsModalScreenCoordinatorType.swift */; }; + 32DB557E22FDADE50016329E /* ServiceTermsModalScreenViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB557322FDADE50016329E /* ServiceTermsModalScreenViewAction.swift */; }; + 32DB557F22FDADE50016329E /* ServiceTermsModalScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DB557422FDADE50016329E /* ServiceTermsModalScreenCoordinator.swift */; }; 32F6B9692270623100BBA352 /* DeviceVerificationDataLoadingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9632270623000BBA352 /* DeviceVerificationDataLoadingCoordinator.swift */; }; 32F6B96A2270623100BBA352 /* DeviceVerificationDataLoadingViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9642270623100BBA352 /* DeviceVerificationDataLoadingViewState.swift */; }; 32F6B96B2270623100BBA352 /* DeviceVerificationDataLoadingViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9652270623100BBA352 /* DeviceVerificationDataLoadingViewAction.swift */; }; @@ -701,6 +712,17 @@ 32D7159E2146CC6F00DF59C9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Vector.strings; sourceTree = ""; }; 32D7159F2146CC7F00DF59C9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 32D715A02146CC8800DF59C9 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + 32DB556922FDADE50016329E /* ServiceTermsModalCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalCoordinatorType.swift; sourceTree = ""; }; + 32DB556A22FDADE50016329E /* ServiceTermsModalCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalCoordinatorBridgePresenter.swift; sourceTree = ""; }; + 32DB556B22FDADE50016329E /* ServiceTermsModalCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalCoordinator.swift; sourceTree = ""; }; + 32DB556D22FDADE50016329E /* ServiceTermsModalScreenViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalScreenViewState.swift; sourceTree = ""; }; + 32DB556E22FDADE50016329E /* ServiceTermsModalScreenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalScreenViewModel.swift; sourceTree = ""; }; + 32DB556F22FDADE50016329E /* ServiceTermsModalScreenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalScreenViewController.swift; sourceTree = ""; }; + 32DB557022FDADE50016329E /* ServiceTermsModalScreenViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ServiceTermsModalScreenViewController.storyboard; sourceTree = ""; }; + 32DB557122FDADE50016329E /* ServiceTermsModalScreenViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalScreenViewModelType.swift; sourceTree = ""; }; + 32DB557222FDADE50016329E /* ServiceTermsModalScreenCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalScreenCoordinatorType.swift; sourceTree = ""; }; + 32DB557322FDADE50016329E /* ServiceTermsModalScreenViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalScreenViewAction.swift; sourceTree = ""; }; + 32DB557422FDADE50016329E /* ServiceTermsModalScreenCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTermsModalScreenCoordinator.swift; sourceTree = ""; }; 32F6B9632270623000BBA352 /* DeviceVerificationDataLoadingCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationDataLoadingCoordinator.swift; sourceTree = ""; }; 32F6B9642270623100BBA352 /* DeviceVerificationDataLoadingViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationDataLoadingViewState.swift; sourceTree = ""; }; 32F6B9652270623100BBA352 /* DeviceVerificationDataLoadingViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationDataLoadingViewAction.swift; sourceTree = ""; }; @@ -1689,6 +1711,40 @@ path = KeyBackup; sourceTree = ""; }; + 32DB556722FDADE50016329E /* ServiceTerms */ = { + isa = PBXGroup; + children = ( + 32DB556822FDADE50016329E /* Modal */, + ); + path = ServiceTerms; + sourceTree = ""; + }; + 32DB556822FDADE50016329E /* Modal */ = { + isa = PBXGroup; + children = ( + 32DB556922FDADE50016329E /* ServiceTermsModalCoordinatorType.swift */, + 32DB556A22FDADE50016329E /* ServiceTermsModalCoordinatorBridgePresenter.swift */, + 32DB556B22FDADE50016329E /* ServiceTermsModalCoordinator.swift */, + 32DB556C22FDADE50016329E /* Modal */, + ); + path = Modal; + sourceTree = ""; + }; + 32DB556C22FDADE50016329E /* Modal */ = { + isa = PBXGroup; + children = ( + 32DB556D22FDADE50016329E /* ServiceTermsModalScreenViewState.swift */, + 32DB556E22FDADE50016329E /* ServiceTermsModalScreenViewModel.swift */, + 32DB556F22FDADE50016329E /* ServiceTermsModalScreenViewController.swift */, + 32DB557022FDADE50016329E /* ServiceTermsModalScreenViewController.storyboard */, + 32DB557122FDADE50016329E /* ServiceTermsModalScreenViewModelType.swift */, + 32DB557222FDADE50016329E /* ServiceTermsModalScreenCoordinatorType.swift */, + 32DB557322FDADE50016329E /* ServiceTermsModalScreenViewAction.swift */, + 32DB557422FDADE50016329E /* ServiceTermsModalScreenCoordinator.swift */, + ); + path = Modal; + sourceTree = ""; + }; 4220F60B660591FD80AF3428 /* Pods */ = { isa = PBXGroup; children = ( @@ -2178,6 +2234,7 @@ B1B5567620EE6C4C00210D55 /* Modules */ = { isa = PBXGroup; children = ( + 32DB556722FDADE50016329E /* ServiceTerms */, 3232AB94225730E100AD6A5C /* DeviceVerification */, B1B556EA20EE6C4C00210D55 /* Main */, B1B556CA20EE6C4C00210D55 /* TabBar */, @@ -3833,6 +3890,7 @@ B1B5574D20EE6C4D00210D55 /* MediaPickerViewController.xib in Resources */, B1B5575020EE6C4D00210D55 /* AuthenticationViewController.xib in Resources */, B14F142E22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.storyboard in Resources */, + 32DB557B22FDADE50016329E /* ServiceTermsModalScreenViewController.storyboard in Resources */, B1B5574320EE6C4D00210D55 /* CallViewController.xib in Resources */, F083BDEA1E7009ED00A9B29C /* ringback.mp3 in Resources */, F083BDF21E7009ED00A9B29C /* GoogleService-Info.plist in Resources */, @@ -4152,6 +4210,7 @@ B19EFA3B21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift in Sources */, B1B9DEDE22E9D9890065E677 /* EmojiServiceType.swift in Sources */, 3232ABA9225730E100AD6A5C /* DeviceVerificationStartViewModel.swift in Sources */, + 32DB557C22FDADE50016329E /* ServiceTermsModalScreenViewModelType.swift in Sources */, B16932FA20F3C51A00746532 /* RecentCellData.m in Sources */, B16932F220F3C49E00746532 /* GroupsDataSource.m in Sources */, B1B5581C20EF625800210D55 /* RoomAvatarTitleView.m in Sources */, @@ -4227,6 +4286,7 @@ B169331420F3CAFC00746532 /* PublicRoomTableViewCell.m in Sources */, 32BF995721FB07A400698084 /* SettingsKeyBackupTableViewSection.swift in Sources */, B14F142F22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModelType.swift in Sources */, + 32DB557822FDADE50016329E /* ServiceTermsModalScreenViewState.swift in Sources */, B1B558E120EF768F00210D55 /* RoomMembershipCollapsedBubbleCell.m in Sources */, B1B5571A20EE6C4D00210D55 /* SettingsViewController.m in Sources */, B1CE9EFD22148703000FAE6A /* SignOutAlertPresenter.swift in Sources */, @@ -4247,6 +4307,7 @@ 3275FD8C21A5A2C500B9C13D /* TermsView.swift in Sources */, B1B9DEE822EB34EF0065E677 /* ReactionHistoryCoordinatorType.swift in Sources */, B14F143122144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewState.swift in Sources */, + 32DB557F22FDADE50016329E /* ServiceTermsModalScreenCoordinator.swift in Sources */, B1098C1121ED07E4000DDA48 /* NavigationRouterType.swift in Sources */, B1B5573D20EE6C4D00210D55 /* WebViewViewController.m in Sources */, 3209451221F1C1430088CAA2 /* BlackTheme.swift in Sources */, @@ -4254,6 +4315,8 @@ 3232ABBC2257BE6500AD6A5C /* DeviceVerificationVerifyViewAction.swift in Sources */, F05927C91FDED836009F2A68 /* MXGroup+Riot.m in Sources */, B1B5594520EF7BD000210D55 /* TableViewCellWithCollectionView.m in Sources */, + 32DB557722FDADE50016329E /* ServiceTermsModalCoordinator.swift in Sources */, + 32DB557922FDADE50016329E /* ServiceTermsModalScreenViewModel.swift in Sources */, 32891D75226728EE00C82226 /* DeviceVerificationDataLoadingViewController.swift in Sources */, 32891D712264DF7B00C82226 /* DeviceVerificationVerifiedViewController.swift in Sources */, F083BDEF1E7009ED00A9B29C /* UINavigationController+Riot.m in Sources */, @@ -4264,6 +4327,7 @@ B1B5571920EE6C4D00210D55 /* LanguagePickerViewController.m in Sources */, 3232AB512256558300AD6A5C /* TemplateScreenViewAction.swift in Sources */, 3232AB4E2256558300AD6A5C /* TemplateScreenViewModelType.swift in Sources */, + 32DB557D22FDADE50016329E /* ServiceTermsModalScreenCoordinatorType.swift in Sources */, B1B5590520EF768F00210D55 /* RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m in Sources */, 3232ABA5225730E100AD6A5C /* DeviceVerificationStartViewModelType.swift in Sources */, B1B558DD20EF768F00210D55 /* RoomIncomingEncryptedTextMsgBubbleCell.m in Sources */, @@ -4274,6 +4338,7 @@ B1D4752721EE4E630067973F /* KeyboardAvoider.swift in Sources */, B1D4752821EE4E630067973F /* KeyboardNotification.swift in Sources */, B1D1BDA622BBAFB500831367 /* ReactionsMenuView.swift in Sources */, + 32DB557A22FDADE50016329E /* ServiceTermsModalScreenViewController.swift in Sources */, B1B9DEF422EB426D0065E677 /* ReactionHistoryViewCell.swift in Sources */, B1B5573C20EE6C4D00210D55 /* MasterTabBarController.m in Sources */, B1DCC61B22E5E17100625807 /* EmojiPickerCoordinator.swift in Sources */, @@ -4447,6 +4512,7 @@ B1B557CC20EF5D8000210D55 /* DirectoryServerTableViewCell.m in Sources */, B1963B2B228F1C4900CBA17F /* BubbleReactionsView.swift in Sources */, B1B5575C20EE6C4D00210D55 /* DirectoryViewController.m in Sources */, + 32DB557622FDADE50016329E /* ServiceTermsModalCoordinatorBridgePresenter.swift in Sources */, B1B558BD20EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.m in Sources */, B1B5577020EE702800210D55 /* WidgetPickerViewController.m in Sources */, B1B558D320EF768F00210D55 /* RoomOutgoingEncryptedTextMsgBubbleCell.m in Sources */, @@ -4462,6 +4528,7 @@ 32B94DF9228EC26400716A26 /* ReactionsMenuViewAction.swift in Sources */, B1B5599420EFC5E400210D55 /* DecryptionFailureTracker.m in Sources */, F083BDF01E7009ED00A9B29C /* UIViewController+RiotSearch.m in Sources */, + 32DB557522FDADE50016329E /* ServiceTermsModalCoordinatorType.swift in Sources */, F083BDF91E7009ED00A9B29C /* RoomEmailInvitation.m in Sources */, B1D211E422C18E3800D939BD /* ReactionsMenuViewModelType.swift in Sources */, 324A2055225FC571004FE8B0 /* DeviceVerificationIncomingViewModelType.swift in Sources */, @@ -4470,6 +4537,7 @@ 3232ABB52257BE6400AD6A5C /* DeviceVerificationVerifyCoordinatorType.swift in Sources */, 32BF994F21FA29A400698084 /* SettingsKeyBackupViewModel.swift in Sources */, B190F55D22CE5A9700AEB493 /* EditHistorySection.swift in Sources */, + 32DB557E22FDADE50016329E /* ServiceTermsModalScreenViewAction.swift in Sources */, 32A6002022C66FCF0042C1D9 /* EditHistoryMessage.swift in Sources */, B1B5574920EE6C4D00210D55 /* RiotSplitViewController.m in Sources */, B1B5574E20EE6C4D00210D55 /* DirectoryServerPickerViewController.m in Sources */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index cc51f3107..eb6f15cb5 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -691,6 +691,13 @@ "gdpr_consent_not_given_alert_message" = "To continue using the %@ homeserver you must review and agree to the terms and conditions."; "gdpr_consent_not_given_alert_review_now_action" = "Review now"; +// Service terms +"service_terms_modal_title" = "Terms Of Service"; +"service_terms_modal_message" = "To continue you need to accept the Terms of this service."; +"service_terms_modal_accept_button" = "Accept"; +"service_terms_modal_description_for_identity_server" = "Be discoverable by others"; +"service_terms_modal_description_for_integration_manager" = "Use Bots, bridges, widgets and sticker packs"; + // Deactivate account "deactivate_account_title" = "Deactivate Account"; diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index 28bbe13d8..07cadf8f5 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -92,6 +92,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: RoomContextualMenuViewController.self) } + internal enum ServiceTermsModalScreenViewController: StoryboardType { + internal static let storyboardName = "ServiceTermsModalScreenViewController" + + internal static let initialScene = InitialSceneType(storyboard: ServiceTermsModalScreenViewController.self) + } internal enum SimpleScreenTemplateViewController: StoryboardType { internal static let storyboardName = "SimpleScreenTemplateViewController" diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 27ddc0ec6..2e97b5642 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2342,6 +2342,26 @@ internal enum VectorL10n { internal static var sending: String { return VectorL10n.tr("Vector", "sending") } + /// Accept + internal static var serviceTermsModalAcceptButton: String { + return VectorL10n.tr("Vector", "service_terms_modal_accept_button") + } + /// Be discoverable by others + internal static var serviceTermsModalDescriptionForIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_for_identity_server") + } + /// Use Bots, bridges, widgets and sticker packs + internal static var serviceTermsModalDescriptionForIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_for_integration_manager") + } + /// To continue you need to accept the Terms of this service. + internal static var serviceTermsModalMessage: String { + return VectorL10n.tr("Vector", "service_terms_modal_message") + } + /// Terms Of Service + internal static var serviceTermsModalTitle: String { + return VectorL10n.tr("Vector", "service_terms_modal_title") + } /// Add email address internal static var settingsAddEmailAddress: String { return VectorL10n.tr("Vector", "settings_add_email_address") diff --git a/Riot/Managers/Widgets/WidgetManager.h b/Riot/Managers/Widgets/WidgetManager.h index a15928789..f8d027185 100644 --- a/Riot/Managers/Widgets/WidgetManager.h +++ b/Riot/Managers/Widgets/WidgetManager.h @@ -56,10 +56,12 @@ typedef enum : NSUInteger WidgetManagerErrorCodeNotEnoughPower, WidgetManagerErrorCodeCreationFailed, WidgetManagerErrorCodeNoIntegrationsServerConfigured, - WidgetManagerErrorCodeFailedToConnectToIntegrationsServer + WidgetManagerErrorCodeFailedToConnectToIntegrationsServer, + WidgetManagerErrorCodeTermsNotSigned } WidgetManagerErrorCode; +FOUNDATION_EXPORT NSString *const WidgetManagerErrorOpenIdTokenKey; /** The `WidgetManager` helps to handle modular widgets. diff --git a/Riot/Managers/Widgets/WidgetManager.m b/Riot/Managers/Widgets/WidgetManager.m index a6d2bdec1..8250567cb 100644 --- a/Riot/Managers/Widgets/WidgetManager.m +++ b/Riot/Managers/Widgets/WidgetManager.m @@ -513,6 +513,8 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; MXHTTPOperation *operation; NSString *userId = mxSession.myUser.userId; + NSLog(@"[WidgetManager] registerForScalarToken"); + WidgetManagerConfig *config = [self configForUser:userId]; if (!config.hasUrls) { @@ -537,15 +539,22 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; NSString *scalarToken; MXJSONModelSetString(scalarToken, JSONResponse[@"scalar_token"]) - config.scalarToken = scalarToken; + config.scalarToken = scalarToken; self->configs[userId] = config; [self saveConfigs]; + + // Validate it (this mostly checks to see if the IM needs us to agree to some terms) + MXHTTPOperation *operation3 = [self validateScalarToken:scalarToken forMXSession:mxSession complete:^(BOOL valid) { - if (success) - { - success(scalarToken); - } + if (success) + { + success(scalarToken); + } + + } failure:failure]; + + [operation mutateTo:operation3]; } failure:^(NSError *error) { NSLog(@"[WidgetManager] registerForScalarToken: Failed to register. Error: %@", error); @@ -622,7 +631,22 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; } else if (failure) { - failure(error); + MXError *mxError = [[MXError alloc] initWithNSError:error]; + if ([mxError.errcode isEqualToString:kMXErrCodeStringTermsNotSigned]) + { + NSLog(@"[WidgetManager] validateScalarToke. Error: Need to accept terms"); + NSError *termsNotSignedError = [NSError errorWithDomain:WidgetManagerErrorDomain + code:WidgetManagerErrorCodeTermsNotSigned + userInfo:@{ + NSLocalizedDescriptionKey:error.userInfo[NSLocalizedDescriptionKey] + }]; + + failure(termsNotSignedError); + } + else + { + failure(error); + } } }]; } diff --git a/Riot/Managers/Widgets/WidgetManagerConfig.swift b/Riot/Managers/Widgets/WidgetManagerConfig.swift index ba6c3ac36..3bd828747 100644 --- a/Riot/Managers/Widgets/WidgetManagerConfig.swift +++ b/Riot/Managers/Widgets/WidgetManagerConfig.swift @@ -36,6 +36,38 @@ class WidgetManagerConfig: NSObject, NSCoding { } } + var baseUrl: NSString? { + // Same comment as https://github.com/matrix-org/matrix-react-sdk/blob/1b0d8510a2ee93beddcd34c2d5770aa9fc76b1d9/src/ScalarAuthClient.js#L108 + // The terms endpoints are new and so live on standard _matrix prefixes, + // but IM rest urls are currently configured with paths, so remove the + // path from the base URL before passing it to the js-sdk + + // We continue to use the full URL for the calls done by + // Riot-iOS, but the standard terms API called + // by the matrix-ios-sdk lives on the standard _matrix path. This means we + // don't support running IMs on a non-root path, but it's the only + // realistic way of transitioning to _matrix paths since configs in + // the wild contain bits of the API path. + + // Once we've fully transitioned to _matrix URLs, we can give people + // a grace period to update their configs, then use the rest url as + // a regular base url. + guard let apiUrl = self.apiUrl as String?, let imApiUrl = URL(string: apiUrl) else { + return nil + } + + guard var baseUrl = URL(string: "/", relativeTo: imApiUrl)?.absoluteString else { + return nil + } + + if baseUrl.hasSuffix("/") { + // SDK doest not like trailing / + baseUrl = String(baseUrl.dropLast()) + } + + return baseUrl as NSString + } + init(apiUrl: NSString?, uiUrl: NSString?) { self.apiUrl = apiUrl self.uiUrl = uiUrl diff --git a/Riot/Modules/Integrations/IntegrationManagerViewController.m b/Riot/Modules/Integrations/IntegrationManagerViewController.m index ff9df08fe..470556421 100644 --- a/Riot/Modules/Integrations/IntegrationManagerViewController.m +++ b/Riot/Modules/Integrations/IntegrationManagerViewController.m @@ -26,7 +26,7 @@ NSString *const kIntegrationManagerMainScreen = nil; NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; -@interface IntegrationManagerViewController () +@interface IntegrationManagerViewController () { MXSession *mxSession; NSString *roomId; @@ -37,6 +37,8 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; MXHTTPOperation *operation; } +@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; + @end @implementation IntegrationManagerViewController @@ -67,10 +69,15 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; operation = nil; } -- (void)viewWillAppear:(BOOL)animated +- (void)viewDidLoad { - [super viewWillAppear:animated]; + [super viewDidLoad]; + [self loadData]; +} + +- (void)loadData +{ if (!self.URL && !operation) { [self startActivityIndicator]; @@ -94,9 +101,17 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; self->operation = nil; [self stopActivityIndicator]; - [self withdrawViewControllerAnimated:YES completion:^{ - [[AppDelegate theDelegate] showErrorAsAlert:error]; - }]; + if ([error.domain isEqualToString:WidgetManagerErrorDomain] + && error.code == WidgetManagerErrorCodeTermsNotSigned) + { + [self presentTerms]; + } + else + { + [self withdrawViewControllerAnimated:YES completion:^{ + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } }]; } } @@ -681,4 +696,39 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; }]; } + +#pragma mark - Service terms + +- (void)presentTerms +{ + WidgetManagerConfig *config = [[WidgetManager sharedManager] configForUser:mxSession.myUser.userId]; + + NSLog(@"[IntegrationManagerVC] presentTerms for %@", config.baseUrl); + + ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession baseUrl:config.baseUrl + serviceType:MXServiceTypeIntegrationManager + accessToken:config.scalarToken]; + + serviceTermsModalCoordinatorBridgePresenter.delegate = self; + + [serviceTermsModalCoordinatorBridgePresenter presentFrom:self animated:YES]; + self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + [self loadData]; + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + [self withdrawViewControllerAnimated:YES completion:nil]; + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Integrations/Widgets/WidgetViewController.m b/Riot/Modules/Integrations/Widgets/WidgetViewController.m index f7ad5c0bd..fb085077c 100644 --- a/Riot/Modules/Integrations/Widgets/WidgetViewController.m +++ b/Riot/Modules/Integrations/Widgets/WidgetViewController.m @@ -19,10 +19,13 @@ #import "AppDelegate.h" #import "IntegrationManagerViewController.h" +#import "Riot-Swift.h" NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse('%@', %@);"; -@interface WidgetViewController () +@interface WidgetViewController () + +@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; @end @@ -363,15 +366,71 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse MXStrongifyAndReturnIfNil(self); NSLog(@"[WidgetVC] fixScalarToken: DONE"); - - self.URL = [self stringByReplacingScalarTokenInString:self.URL byScalarToken:scalarToken]; - - self->webView.hidden = NO; + [self loadDataWithScalarToken:scalarToken]; } failure:^(NSError *error) { NSLog(@"[WidgetVC] fixScalarToken: Error: %@", error); - [self showErrorAsAlert:error]; + + if ([error.domain isEqualToString:WidgetManagerErrorDomain] + && error.code == WidgetManagerErrorCodeTermsNotSigned) + { + [self presentTerms]; + } + else + { + [self showErrorAsAlert:error]; + } }]; } +- (void)loadDataWithScalarToken:(NSString*)scalarToken +{ + self.URL = [self stringByReplacingScalarTokenInString:self.URL byScalarToken:scalarToken]; + + self->webView.hidden = NO; +} + + + +#pragma mark - Service terms + +- (void)presentTerms +{ + if (self.serviceTermsModalCoordinatorBridgePresenter) + { + return; + } + + WidgetManagerConfig *config = [[WidgetManager sharedManager] configForUser:widget.mxSession.myUser.userId]; + + NSLog(@"[WidgetVC] presentTerms for %@", config.baseUrl); + + ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:widget.mxSession baseUrl:config.baseUrl + serviceType:MXServiceTypeIntegrationManager accessToken:config.scalarToken]; + serviceTermsModalCoordinatorBridgePresenter.delegate = self; + + [serviceTermsModalCoordinatorBridgePresenter presentFrom:self animated:YES]; + self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + MXWeakify(self); + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + MXStrongifyAndReturnIfNil(self); + + WidgetManagerConfig *config = [[WidgetManager sharedManager] configForUser:self->widget.mxSession.myUser.userId]; + [self loadDataWithScalarToken:config.scalarToken]; + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + [self withdrawViewControllerAnimated:YES completion:nil]; + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift new file mode 100644 index 000000000..d10923dee --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift @@ -0,0 +1,73 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +final class ServiceTermsModalScreenCoordinator: ServiceTermsModalScreenCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private var serviceTermsModalScreenViewModel: ServiceTermsModalScreenViewModelType + private let serviceTermsModalScreenViewController: ServiceTermsModalScreenViewController + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + // MARK: Public + + weak var delegate: ServiceTermsModalScreenCoordinatorDelegate? + + // MARK: - Setup + + init(serviceTerms: MXServiceTerms) { + + let serviceTermsModalScreenViewModel = ServiceTermsModalScreenViewModel(serviceTerms: serviceTerms) + let serviceTermsModalScreenViewController = ServiceTermsModalScreenViewController.instantiate(with: serviceTermsModalScreenViewModel) + self.serviceTermsModalScreenViewModel = serviceTermsModalScreenViewModel + self.serviceTermsModalScreenViewController = serviceTermsModalScreenViewController + } + + // MARK: - Public methods + + func start() { + self.serviceTermsModalScreenViewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.serviceTermsModalScreenViewController + } +} + +// MARK: - ServiceTermsModalScreenViewModelCoordinatorDelegate +extension ServiceTermsModalScreenCoordinator: ServiceTermsModalScreenViewModelCoordinatorDelegate { + + func serviceTermsModalScreenViewModelDidAccept(_ viewModel: ServiceTermsModalScreenViewModelType) { + self.delegate?.serviceTermsModalScreenCoordinatorDidAccept(self) + } + + func serviceTermsModalScreenViewModel(_ coordinator: ServiceTermsModalScreenViewModelType, displayPolicy policy: MXLoginPolicyData) { + self.delegate?.serviceTermsModalScreenCoordinator(self, displayPolicy: policy) + } + + func serviceTermsModalScreenViewModelDidCancel(_ viewModel: ServiceTermsModalScreenViewModelType) { + self.delegate?.serviceTermsModalScreenCoordinatorDidCancel(self) + } +} diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift new file mode 100644 index 000000000..9cffa38d7 --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift @@ -0,0 +1,30 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol ServiceTermsModalScreenCoordinatorDelegate: class { + func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) + func serviceTermsModalScreenCoordinator(_ coordinator: ServiceTermsModalScreenCoordinatorType, displayPolicy policy: MXLoginPolicyData) + func serviceTermsModalScreenCoordinatorDidCancel(_ coordinator: ServiceTermsModalScreenCoordinatorType) +} + +/// `ServiceTermsModalScreenCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. +protocol ServiceTermsModalScreenCoordinatorType: Coordinator, Presentable { + var delegate: ServiceTermsModalScreenCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift new file mode 100644 index 000000000..62a856c0b --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift @@ -0,0 +1,27 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// ServiceTermsModalScreenViewController view actions exposed to view model +enum ServiceTermsModalScreenViewAction { + case load + case display(MXLoginPolicyData) + case accept + case cancel +} diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard new file mode 100644 index 000000000..9631169c5 --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift new file mode 100644 index 000000000..ab365c476 --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift @@ -0,0 +1,293 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +final class ServiceTermsModalScreenViewController: UIViewController { + + // MARK: - Constants + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var scrollView: UIScrollView! + + @IBOutlet private weak var messageLabel: UILabel! + @IBOutlet private weak var tableView: UITableView! + @IBOutlet private weak var acceptButton: UIButton! + + // MARK: Private + + private var viewModel: ServiceTermsModalScreenViewModelType! + private var theme: Theme! + private var errorPresenter: MXKErrorPresentation! + private var activityPresenter: ActivityIndicatorPresenter! + + private var policies: [MXLoginPolicyData] = [] + + /// Policies checked by the end user + private var checkedPolicies: Set = [] + + // MARK: - Setup + + class func instantiate(with viewModel: ServiceTermsModalScreenViewModelType) -> ServiceTermsModalScreenViewController { + let viewController = StoryboardScene.ServiceTermsModalScreenViewController.initialScene.instantiate() + viewController.viewModel = viewModel + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + self.title = VectorL10n.serviceTermsModalTitle + + self.setupViews() + self.activityPresenter = ActivityIndicatorPresenter() + self.errorPresenter = MXKErrorAlertPresentation() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + + self.viewModel.viewDelegate = self + + self.viewModel.process(viewAction: .load) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return self.theme.statusBarStyle + } + + // MARK: - Private + + private func update(theme: Theme) { + self.theme = theme + + self.view.backgroundColor = theme.headerBackgroundColor + + if let navigationBar = self.navigationController?.navigationBar { + theme.applyStyle(onNavigationBar: navigationBar) + } + + self.messageLabel.textColor = theme.textPrimaryColor + + self.acceptButton.backgroundColor = theme.backgroundColor + theme.applyStyle(onButton: self.acceptButton) + + self.refreshViews() + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + private func setupViews() { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + + self.navigationItem.rightBarButtonItem = cancelBarButtonItem + + self.setupTableView() + self.scrollView.keyboardDismissMode = .interactive + + self.messageLabel.text = VectorL10n.serviceTermsModalMessage + + self.acceptButton.setTitle(VectorL10n.serviceTermsModalAcceptButton, for: .normal) + self.acceptButton.setTitle(VectorL10n.serviceTermsModalAcceptButton, for: .highlighted) + self.refreshAcceptButton() + } + + private func setupTableView() { + self.tableView.delegate = self + self.tableView.dataSource = self + self.tableView.separatorStyle = .none + self.tableView.alwaysBounceVertical = false + self.tableView.backgroundColor = .clear + self.tableView.register(TableViewCellWithCheckBoxAndLabel.nib(), forCellReuseIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier()) + } + + private func render(viewState: ServiceTermsModalScreenViewState) { + switch viewState { + case .loading: + self.renderLoading() + case .loaded(let policies, let alreadyAcceptedPoliciesUrls): + self.renderLoaded(policies: policies, alreadyAcceptedPoliciesUrls: alreadyAcceptedPoliciesUrls) + case .accepted: + self.renderAccepted() + case .error(let error): + self.render(error: error) + } + } + + private func renderLoading() { + self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + } + + private func renderLoaded(policies: [MXLoginPolicyData], alreadyAcceptedPoliciesUrls: [String]) { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + + self.policies = policies + self.updateCheckedPolicies(with: alreadyAcceptedPoliciesUrls) + + self.refreshViews() + } + + private func renderAccepting() { + self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + } + + private func renderAccepted() { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + } + + private func render(error: Error) { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) + } + + private func refreshViews() { + self.tableView.reloadData() + self.refreshAcceptButton() + } + + private func refreshAcceptButton() { + // Enable the button only if the user has accepted all policies + self.acceptButton.isEnabled = (self.policies.count == self.checkedPolicies.count) + } + + // Pre-check policies already accepted by the user + private func updateCheckedPolicies(with acceptedPoliciesUrls: [String]) { + for url in acceptedPoliciesUrls { + if let policyIndex = self.policies.firstIndex(where: { $0.url == url }) { + checkedPolicies.insert(policyIndex) + } + } + } + + + // MARK: - Actions + + @IBAction private func acceptButtonAction(_ sender: Any) { + self.viewModel.process(viewAction: .accept) + } + + private func cancelButtonAction() { + self.viewModel.process(viewAction: .cancel) + } + + @objc private func didTapCheckbox(sender: UITapGestureRecognizer) { + + guard let policyIndex = sender.view?.tag else { + return + } + + if self.checkedPolicies.contains(policyIndex) { + self.checkedPolicies.remove(policyIndex) + } else { + checkedPolicies.insert(policyIndex) + } + + self.refreshViews() + } +} + + +// MARK: - ServiceTermsModalScreenViewModelViewDelegate +extension ServiceTermsModalScreenViewController: ServiceTermsModalScreenViewModelViewDelegate { + + func serviceTermsModalScreenViewModel(_ viewModel: ServiceTermsModalScreenViewModelType, didUpdateViewState viewSate: ServiceTermsModalScreenViewState) { + self.render(viewState: viewSate) + } +} + +// MARK: - UITableViewDataSource + +extension ServiceTermsModalScreenViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.policies.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier(), for: indexPath) as? TableViewCellWithCheckBoxAndLabel else { + fatalError("\(String(describing: TableViewCellWithCheckBoxAndLabel.self)) should be registered") + } + + let policy = policies[indexPath.row] + let checked = checkedPolicies.contains(indexPath.row) + + cell.label.attributedText = self.cellLabel(for: policy) + cell.label.font = .systemFont(ofSize: 15) + cell.isEnabled = checked + cell.accessoryType = .disclosureIndicator + cell.backgroundColor = self.theme.backgroundColor + + if let checkBox = cell.checkBox, checkBox.gestureRecognizers?.isEmpty ?? true { + let gesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapCheckbox)) + gesture.numberOfTapsRequired = 1 + gesture.numberOfTouchesRequired = 1 + + checkBox.isUserInteractionEnabled = true + checkBox.tag = indexPath.row + checkBox.addGestureRecognizer(gesture) + } + + return cell + } + + func cellLabel(for policy: MXLoginPolicyData) -> NSAttributedString { + + // TableViewCellWithCheckBoxAndLabel does not have a detailTextLabel + // Do it by hand + + var labelDetail: String = "" + switch self.viewModel.serviceType { + case MXServiceTypeIdentityService: + labelDetail = VectorL10n.serviceTermsModalDescriptionForIdentityServer + case MXServiceTypeIntegrationManager: + labelDetail = VectorL10n.serviceTermsModalDescriptionForIntegrationManager + default: break + } + + let label = NSMutableAttributedString(string: policy.name, + attributes: [.foregroundColor: theme.textPrimaryColor]) + label.append(NSAttributedString(string: "\n")) + label.append(NSAttributedString(string: labelDetail, + attributes: [.foregroundColor: theme.textSecondaryColor])) + + return label + } +} + +extension ServiceTermsModalScreenViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let policy = policies[indexPath.row] + self.viewModel.process(viewAction: .display(policy)) + } +} diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift new file mode 100644 index 000000000..7d0545c96 --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift @@ -0,0 +1,128 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +final class ServiceTermsModalScreenViewModel: ServiceTermsModalScreenViewModelType { + + // MARK: - Properties + + // MARK: Private + + private let serviceTerms: MXServiceTerms + + // MARK: Public + + var serviceType: MXServiceType { + return serviceTerms.serviceType + } + var policies: [MXLoginPolicyData]? + var alreadyAcceptedPoliciesUrls: [String] = [] + + weak var viewDelegate: ServiceTermsModalScreenViewModelViewDelegate? + weak var coordinatorDelegate: ServiceTermsModalScreenViewModelCoordinatorDelegate? + + // MARK: - Setup + + init(serviceTerms: MXServiceTerms) { + self.serviceTerms = serviceTerms + } + + deinit { + } + + // MARK: - Public + + func process(viewAction: ServiceTermsModalScreenViewAction) { + switch viewAction { + case .load: + self.loadTerms() + case .display(let policy): + self.coordinatorDelegate?.serviceTermsModalScreenViewModel(self, displayPolicy: policy) + case .accept: + self.acceptTerms() + case .cancel: + self.coordinatorDelegate?.serviceTermsModalScreenViewModelDidCancel(self) + } + } + + // MARK: - Private + + private func loadTerms() { + + self.update(viewState: .loading) + + self.serviceTerms.terms({ [weak self] (terms, alreadyAcceptedTermsUrls) in + guard let self = self else { + return + } + + let policies = self.processTerms(terms: terms) + self.policies = policies + self.alreadyAcceptedPoliciesUrls = alreadyAcceptedTermsUrls ?? [] + self.update(viewState: .loaded(policies: policies, alreadyAcceptedPoliciesUrls: self.alreadyAcceptedPoliciesUrls)) + + }, failure: { [weak self] error in + guard let self = self else { + return + } + + self.update(viewState: .error(error)) + }) + } + + private func acceptTerms() { + + self.update(viewState: .loading) + + self.serviceTerms.agree(toTerms: self.termsUrls, success: { [weak self] in + guard let self = self else { + return + } + self.update(viewState: .accepted) + self.coordinatorDelegate?.serviceTermsModalScreenViewModelDidAccept(self) + + }, failure: { [weak self] (error) in + guard let self = self else { + return + } + + self.update(viewState: .error(error)) + }) + } + + private func processTerms(terms: MXLoginTerms?) -> [MXLoginPolicyData] { + if let policies = terms?.policiesData(forLanguage: Bundle.mxk_language(), defaultLanguage: Bundle.mxk_fallbackLanguage()) { + return policies + } else { + print("[ServiceTermsModalScreenViewModel] processTerms: Error: No terms for \(String(describing: terms))") + return [] + } + } + + private var termsUrls: [String] { + guard let policies = self.policies else { + return [] + } + return policies.map({ return $0.url }) + } + + private func update(viewState: ServiceTermsModalScreenViewState) { + self.viewDelegate?.serviceTermsModalScreenViewModel(self, didUpdateViewState: viewState) + } +} diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModelType.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModelType.swift new file mode 100644 index 000000000..aaa4d2dd4 --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModelType.swift @@ -0,0 +1,42 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol ServiceTermsModalScreenViewModelViewDelegate: class { + func serviceTermsModalScreenViewModel(_ viewModel: ServiceTermsModalScreenViewModelType, didUpdateViewState viewSate: ServiceTermsModalScreenViewState) +} + +protocol ServiceTermsModalScreenViewModelCoordinatorDelegate: class { + func serviceTermsModalScreenViewModel(_ coordinator: ServiceTermsModalScreenViewModelType, displayPolicy policy: MXLoginPolicyData) + func serviceTermsModalScreenViewModelDidAccept(_ viewModel: ServiceTermsModalScreenViewModelType) + func serviceTermsModalScreenViewModelDidCancel(_ viewModel: ServiceTermsModalScreenViewModelType) +} + +/// Protocol describing the view model used by `ServiceTermsModalScreenViewController` +protocol ServiceTermsModalScreenViewModelType { + + var serviceType: MXServiceType { get } + var policies: [MXLoginPolicyData]? { get set } + var alreadyAcceptedPoliciesUrls: [String] { get set } + + var viewDelegate: ServiceTermsModalScreenViewModelViewDelegate? { get set } + var coordinatorDelegate: ServiceTermsModalScreenViewModelCoordinatorDelegate? { get set } + + func process(viewAction: ServiceTermsModalScreenViewAction) +} diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewState.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewState.swift new file mode 100644 index 000000000..93f9c942d --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewState.swift @@ -0,0 +1,27 @@ +// File created from ScreenTemplate +// $ createScreen.sh Modal/Show ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// ServiceTermsModalScreenViewController view state +enum ServiceTermsModalScreenViewState { + case loading + case loaded(policies: [MXLoginPolicyData], alreadyAcceptedPoliciesUrls: [String]) + case accepted + case error(Error) +} diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift new file mode 100644 index 000000000..433d02baa --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift @@ -0,0 +1,104 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Modal ServiceTermsModal ServiceTermsModalScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +@objcMembers +final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private let navigationRouter: NavigationRouterType + private let session: MXSession + private let serviceTerms: MXServiceTerms + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: ServiceTermsModalCoordinatorDelegate? + + // MARK: - Setup + init(session: MXSession, baseUrl: String, serviceType: MXServiceType, accessToken: String) { + self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController()) + self.session = session + self.serviceTerms = MXServiceTerms(baseUrl: baseUrl, serviceType: serviceType, matrixSession: session, accessToken: accessToken) + } + + // MARK: - Public methods + + func start() { + let rootCoordinator = self.createServiceTermsModalLoadTermsScreenCoordinator() + + rootCoordinator.start() + + self.add(childCoordinator: rootCoordinator) + + self.navigationRouter.setRootModule(rootCoordinator) + } + + func toPresentable() -> UIViewController { + return self.navigationRouter.toPresentable() + } + + // MARK: - Private methods + + private func createServiceTermsModalLoadTermsScreenCoordinator() -> ServiceTermsModalScreenCoordinator { + let coordinator = ServiceTermsModalScreenCoordinator(serviceTerms: self.serviceTerms) + coordinator.delegate = self + return coordinator + } + + private func showPolicy(policy: MXLoginPolicyData) { + // Display the policy webpage into our webview + let webViewViewController: WebViewViewController = WebViewViewController(url: policy.url) + webViewViewController.title = policy.name + + let leftBarButtonItem: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "back_icon"), style: .plain, target: self, action: #selector(didTapCancelOnPolicyScreen)) + webViewViewController.navigationItem.leftBarButtonItem = leftBarButtonItem + + self.navigationRouter.push(webViewViewController, animated: true, popCompletion: nil) + } + + private func removePolicyScreen() { + self.navigationRouter.popModule(animated: true) + } + + @objc private func didTapCancelOnPolicyScreen() { + self.removePolicyScreen() + } +} + +// MARK: - ServiceTermsModalLoadTermsScreenCoordinatorDelegate +extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelegate { + + func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) { + self.delegate?.serviceTermsModalCoordinatorDidAccept(self) + } + + func serviceTermsModalScreenCoordinator(_ coordinator: ServiceTermsModalScreenCoordinatorType, displayPolicy policy: MXLoginPolicyData) { + self.showPolicy(policy: policy) + } + + func serviceTermsModalScreenCoordinatorDidCancel(_ coordinator: ServiceTermsModalScreenCoordinatorType) { + self.delegate?.serviceTermsModalCoordinatorDidCancel(self) + } +} diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..22cda942b --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift @@ -0,0 +1,95 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Modal ServiceTermsModal ServiceTermsModalLoadTermsScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +@objc protocol ServiceTermsModalCoordinatorBridgePresenterDelegate { + func serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) + func serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) +} + +/// ServiceTermsModalCoordinatorBridgePresenter enables to start ServiceTermsModalCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +@objcMembers +final class ServiceTermsModalCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private let baseUrl: String + private let serviceType: MXServiceType + private let accessToken: String + private var coordinator: ServiceTermsModalCoordinator? + + // MARK: Public + + weak var delegate: ServiceTermsModalCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(session: MXSession, baseUrl: String, serviceType: MXServiceType, accessToken: String) { + self.session = session + self.baseUrl = baseUrl + self.serviceType = serviceType + self.accessToken = accessToken + super.init() + } + + // MARK: - Public + + // NOTE: Default value feature is not compatible with Objective-C. + // func present(from viewController: UIViewController, animated: Bool) { + // self.present(from: viewController, animated: animated) + // } + + func present(from viewController: UIViewController, animated: Bool) { + let serviceTermsModalCoordinator = ServiceTermsModalCoordinator(session: self.session, baseUrl: self.baseUrl, serviceType: self.serviceType, accessToken: accessToken) + serviceTermsModalCoordinator.delegate = self + viewController.present(serviceTermsModalCoordinator.toPresentable(), animated: animated, completion: nil) + serviceTermsModalCoordinator.start() + + self.coordinator = serviceTermsModalCoordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + } +} + +// MARK: - ServiceTermsModalCoordinatorDelegate +extension ServiceTermsModalCoordinatorBridgePresenter: ServiceTermsModalCoordinatorDelegate { + + func serviceTermsModalCoordinatorDidAccept(_ coordinator: ServiceTermsModalCoordinatorType) { + self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept(self) + } + + func serviceTermsModalCoordinatorDidCancel(_ coordinator: ServiceTermsModalCoordinatorType) { + self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel(self) + } +} diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift new file mode 100644 index 000000000..27f7b4f8d --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift @@ -0,0 +1,29 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Modal ServiceTermsModal ServiceTermsModalLoadTermsScreen +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol ServiceTermsModalCoordinatorDelegate: class { + func serviceTermsModalCoordinatorDidAccept(_ coordinator: ServiceTermsModalCoordinatorType) + func serviceTermsModalCoordinatorDidCancel(_ coordinator: ServiceTermsModalCoordinatorType) +} + +/// `ServiceTermsModalCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. +protocol ServiceTermsModalCoordinatorType: Coordinator, Presentable { + var delegate: ServiceTermsModalCoordinatorDelegate? { get } +}