diff --git a/CHANGES.rst b/CHANGES.rst index da85bec4d..05ffb0c64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ Improvements: * Integrations: Use the integrations manager provided by the homeserver admin via .well-known (#2815). * i18n: Add Welsh (cy). * SerializationService: Add deserialisation of Any. + * RiotSharedSettings: New class to handle user settings shared accross Riot apps. + * Widgets: Check user permission before opening a widget (TODO design: #2833). Changes in 0.10.2 (2019-11-15) =============================================== diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 54a7f96dd..fd3d52b2d 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -70,6 +70,8 @@ 3275FD8C21A5A2C500B9C13D /* TermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3275FD8B21A5A2C500B9C13D /* TermsView.swift */; }; 3281BCF72201FA4200F4A383 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3281BCF62201FA4200F4A383 /* UIControl.swift */; }; 3284A35120A07C210044F922 /* postMessageAPI.js in Resources */ = {isa = PBXBuildFile; fileRef = 3284A35020A07C210044F922 /* postMessageAPI.js */; }; + 32863A5A2384070300D07C4A /* RiotSharedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32863A592384070300D07C4A /* RiotSharedSettings.swift */; }; + 32863A5C2384074C00D07C4A /* RiotSettingAllowedWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32863A5B2384074C00D07C4A /* RiotSettingAllowedWidgets.swift */; }; 32891D6B2264CBA300C82226 /* SimpleScreenTemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32891D692264CBA300C82226 /* SimpleScreenTemplateViewController.swift */; }; 32891D6C2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32891D6A2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard */; }; 32891D702264DF7B00C82226 /* DeviceVerificationVerifiedViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32891D6E2264DF7B00C82226 /* DeviceVerificationVerifiedViewController.storyboard */; }; @@ -706,6 +708,8 @@ 3275FD8B21A5A2C500B9C13D /* TermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsView.swift; sourceTree = ""; }; 3281BCF62201FA4200F4A383 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 3284A35020A07C210044F922 /* postMessageAPI.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = postMessageAPI.js; sourceTree = ""; }; + 32863A592384070300D07C4A /* RiotSharedSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RiotSharedSettings.swift; sourceTree = ""; }; + 32863A5B2384074C00D07C4A /* RiotSettingAllowedWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiotSettingAllowedWidgets.swift; sourceTree = ""; }; 32891D692264CBA300C82226 /* SimpleScreenTemplateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleScreenTemplateViewController.swift; sourceTree = ""; }; 32891D6A2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SimpleScreenTemplateViewController.storyboard; sourceTree = ""; }; 32891D6E2264DF7B00C82226 /* DeviceVerificationVerifiedViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DeviceVerificationVerifiedViewController.storyboard; sourceTree = ""; }; @@ -1685,6 +1689,23 @@ path = Incoming; sourceTree = ""; }; + 32863A572384070300D07C4A /* Shared */ = { + isa = PBXGroup; + children = ( + 32863A582384070300D07C4A /* JSONModels */, + 32863A592384070300D07C4A /* RiotSharedSettings.swift */, + ); + path = Shared; + sourceTree = ""; + }; + 32863A582384070300D07C4A /* JSONModels */ = { + isa = PBXGroup; + children = ( + 32863A5B2384074C00D07C4A /* RiotSettingAllowedWidgets.swift */, + ); + path = JSONModels; + sourceTree = ""; + }; 32891D682264C6A000C82226 /* SimpleScreenTemplate */ = { isa = PBXGroup; children = ( @@ -3417,6 +3438,7 @@ B1B5598A20EFC42100210D55 /* Settings */ = { isa = PBXGroup; children = ( + 32863A572384070300D07C4A /* Shared */, B1B5597F20EFC3DF00210D55 /* RiotSettings.swift */, ); path = Settings; @@ -4413,6 +4435,7 @@ B1CE9EFD22148703000FAE6A /* SignOutAlertPresenter.swift in Sources */, 32F6B9692270623100BBA352 /* DeviceVerificationDataLoadingCoordinator.swift in Sources */, B125FE1D231D5DE400B72806 /* SettingsDiscoveryViewModel.swift in Sources */, + 32863A5A2384070300D07C4A /* RiotSharedSettings.swift in Sources */, B1B5594720EF7BD000210D55 /* RoomCollectionViewCell.m in Sources */, B10CFBC32268D99D00A5842E /* JitsiService.swift in Sources */, B1B558C120EF768F00210D55 /* RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.m in Sources */, @@ -4514,6 +4537,7 @@ B14F143522144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.swift in Sources */, B1DCC61E22E5E17100625807 /* EmojiPickerViewModel.swift in Sources */, B1B5574F20EE6C4D00210D55 /* RoomsViewController.m in Sources */, + 32863A5C2384074C00D07C4A /* RiotSettingAllowedWidgets.swift in Sources */, B1B9DEDA22E9B7350065E677 /* SerializationService.swift in Sources */, B1B5572520EE6C4D00210D55 /* RoomMessagesSearchViewController.m in Sources */, B139C22121FE5D9D00BB68EC /* KeyBackupRecoverFromPassphraseViewState.swift in Sources */, diff --git a/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift new file mode 100644 index 000000000..700ac8424 --- /dev/null +++ b/Riot/Managers/Settings/Shared/JSONModels/RiotSettingAllowedWidgets.swift @@ -0,0 +1,36 @@ +/* + 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 + +/// Model for "im.vector.setting.allowed_widgets" +/// https://github.com/vector-im/riot-meta/blob/master/spec/settings.md#tracking-which-widgets-the-user-has-allowed-to-load +struct RiotSettingAllowedWidgets { + let widgets: [String: Bool] +} + +extension RiotSettingAllowedWidgets: Decodable { + enum CodingKeys: String, CodingKey { + case widgets + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let widgets = try container.decode([String: Bool].self, forKey: .widgets) + + self.init(widgets: widgets) + } +} diff --git a/Riot/Managers/Settings/Shared/RiotSharedSettings.swift b/Riot/Managers/Settings/Shared/RiotSharedSettings.swift new file mode 100644 index 000000000..6b4574a87 --- /dev/null +++ b/Riot/Managers/Settings/Shared/RiotSharedSettings.swift @@ -0,0 +1,110 @@ +/* + 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 MatrixSDK + +@objc enum WidgetPermission: Int { + case undefined + case granted + case declined +} + +/// Shared user settings across all Riot clients. +/// It implements https://github.com/vector-im/riot-meta/blob/master/spec/settings.md +@objcMembers +class RiotSharedSettings: NSObject { + + // MARK: - Constants + private enum Settings { + static let breadcrumbs = "im.vector.setting.breadcrumbs" + static let integrationProvisioning = "im.vector.setting.integration_provisioning" + static let allowedWidgets = "im.vector.setting.allowed_widgets" + } + + + // MARK: - Properties + // MARK: Private + private let session: MXSession + private lazy var serializationService: SerializationServiceType = SerializationService() + + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + } + + + // MARK: - Public + + // MARK: Allowed widgets + func permissionFor(widget: Widget) -> WidgetPermission { + guard let allowedWidgets = getAllowedWidgets() else { + return .undefined + } + + return allowedWidgets.widgets[widget.widgetEvent.eventId] == true ? .granted : .declined + } + + func getAllowedWidgets() -> RiotSettingAllowedWidgets? { + guard let allowedWidgetsDict = getAccountData(forEventType: Settings.allowedWidgets) else { + return nil + } + + do { + let allowedWidgets: RiotSettingAllowedWidgets = try serializationService.deserialize(allowedWidgetsDict) + return allowedWidgets + } catch { + return nil + } + } + + @discardableResult func setPermissionFor(widget: Widget, + permission: WidgetPermission, + success: @escaping () -> Void, + failure: @escaping (Error?) -> Void) + -> MXHTTPOperation? { + + guard let widgetEventId = widget.widgetEvent.eventId else { + return nil + } + + var widgets = getAllowedWidgets()?.widgets ?? [:] + + switch permission { + case .undefined: + widgets.removeValue(forKey: widgetEventId) + case .granted: + widgets[widgetEventId] = true + case .declined: + widgets[widgetEventId] = false + } + + // Update only the "widgets" field in the account data + var allowedWidgetsDict = getAccountData(forEventType: Settings.allowedWidgets) ?? [:] + allowedWidgetsDict[RiotSettingAllowedWidgets.CodingKeys.widgets.rawValue] = widgets + + return session.setAccountData(allowedWidgetsDict, forType: Settings.allowedWidgets, success: success, failure: failure) + } + + + // MARK: - Private + private func getAccountData(forEventType eventType: String) -> [String: Any]? { + return session.accountData.accountData(forEventType: eventType) as? [String: Any] + } +} diff --git a/Riot/Modules/Integrations/Widgets/WidgetViewController.m b/Riot/Modules/Integrations/Widgets/WidgetViewController.m index 81f888184..677c3d41c 100644 --- a/Riot/Modules/Integrations/Widgets/WidgetViewController.m +++ b/Riot/Modules/Integrations/Widgets/WidgetViewController.m @@ -26,6 +26,7 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse @interface WidgetViewController () @property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; +@property (nonatomic, strong) NSString *widgetUrl; @end @@ -34,9 +35,10 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse - (instancetype)initWithUrl:(NSString*)widgetUrl forWidget:(Widget*)theWidget { - self = [super initWithURL:widgetUrl]; + self = [super initWithURL:nil]; if (self) { + self.widgetUrl = widgetUrl; widget = theWidget; } return self; @@ -57,6 +59,23 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse } } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + // Check widget permission before opening the widget + [self checkWidgetPermissionWithCompletion:^(BOOL granted) { + if (granted) + { + self.URL = self.widgetUrl; + } + else + { + [self withdrawViewControllerAnimated:YES completion:nil]; + } + }]; +} + - (void)showErrorAsAlert:(NSError*)error { NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey]; @@ -94,6 +113,66 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse [self presentViewController:alert animated:YES completion:nil]; } + +#pragma mark - Widget Permission + +- (void)checkWidgetPermissionWithCompletion:(void (^)(BOOL granted))completion +{ + // Check permission in user settings + MXSession *session = widget.mxSession; + + __block RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:session]; + + WidgetPermission permission = [sharedSettings permissionForWidget:widget]; + if (permission == WidgetPermissionGranted) + { + completion(YES); + } + else + { + // Note: ask permission again if the user previously declined it + [self askPermissionWithCompletion:^(BOOL granted) { + // Update the settings in user account data in parallel + [sharedSettings setPermissionForWidget:self.widget + permission:granted ? WidgetPermissionGranted : WidgetPermissionDeclined + success:^ + { + sharedSettings = nil; + } + failure:^(NSError * _Nullable error) + { + NSLog(@"[WidgetVC] setPermissionForWidget failed. Error: %@", error); + sharedSettings = nil; + }]; + + completion(granted); + }]; + } +} + +- (void)askPermissionWithCompletion:(void (^)(BOOL granted))completion +{ + // TODO: Implement the design + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Load Widget" + message:@"blabla" + preferredStyle:UIAlertControllerStyleAlert]; + + [alert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"continue"] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + completion(YES); + }]]; + + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"decline", @"Vector", nil) + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + completion(NO); + }]]; + + [self presentViewController:alert animated:YES completion:nil]; +} + + #pragma mark - WKNavigationDelegate - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation