diff --git a/Podfile b/Podfile index 3d5f36b11..515dc2228 100644 --- a/Podfile +++ b/Podfile @@ -65,6 +65,7 @@ end abstract_target 'RiotPods' do pod 'GBDeviceInfo', '~> 5.2.0' + pod 'Reusable', '~> 4.0' # Piwik for analytics # While https://github.com/matomo-org/matomo-sdk-ios/pull/223 is not released, use the PR branch diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index dc1207f6f..78cc38375 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -12,7 +12,9 @@ 24EEE5A31F23A8C300B3C705 /* AvatarGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BC111E7009EC00A9B29C /* AvatarGenerator.m */; }; 3233F7461F3497E2006ACA81 /* JitsiMeet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3233F7441F3497DA006ACA81 /* JitsiMeet.framework */; }; 3233F7471F3497E2006ACA81 /* JitsiMeet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3233F7441F3497DA006ACA81 /* JitsiMeet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3275FD8C21A5A2C500B9C13D /* TermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3275FD8B21A5A2C500B9C13D /* TermsView.swift */; }; 3284A35120A07C210044F922 /* postMessageAPI.js in Resources */ = {isa = PBXBuildFile; fileRef = 3284A35020A07C210044F922 /* postMessageAPI.js */; }; + 32B1FEDB21A46F2C00637127 /* TermsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32B1FEDA21A46F2C00637127 /* TermsView.xib */; }; 358DB9429359F97520545D35 /* Pods_RiotPods_RiotShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5856BA7A55E53C0AEAFC084 /* Pods_RiotPods_RiotShareExtension.framework */; }; 89C94E649229EA68AE787E9E /* Pods_RiotPods_Riot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2510A69B4A681C1FEC36E848 /* Pods_RiotPods_Riot.framework */; }; 926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */; }; @@ -385,7 +387,9 @@ 3267EFB420E379FD00FF1CAA /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 3267EFB520E379FD00FF1CAA /* AUTHORS.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = AUTHORS.rst; sourceTree = ""; }; 3267EFB620E379FD00FF1CAA /* README.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.rst; sourceTree = ""; }; + 3275FD8B21A5A2C500B9C13D /* TermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsView.swift; sourceTree = ""; }; 3284A35020A07C210044F922 /* postMessageAPI.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = postMessageAPI.js; sourceTree = ""; }; + 32B1FEDA21A46F2C00637127 /* TermsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TermsView.xib; sourceTree = ""; }; 32BDC9A1211C2C870064AF51 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/InfoPlist.strings; sourceTree = ""; }; 32BDC9A2211C2C870064AF51 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/Localizable.strings; sourceTree = ""; }; 32BDC9A3211C2C870064AF51 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/Vector.strings; sourceTree = ""; }; @@ -1892,6 +1896,8 @@ B1B5579720EF575A00210D55 /* ForgotPasswordInputsView.h */, B1B5579420EF575A00210D55 /* ForgotPasswordInputsView.m */, B1B5579820EF575A00210D55 /* ForgotPasswordInputsView.xib */, + 32B1FEDA21A46F2C00637127 /* TermsView.xib */, + 3275FD8B21A5A2C500B9C13D /* TermsView.swift */, ); path = Views; sourceTree = ""; @@ -2768,6 +2774,7 @@ B1B558D920EF768F00210D55 /* RoomOutgoingEncryptedAttachmentBubbleCell.xib in Resources */, B1B5573020EE6C4D00210D55 /* BugReportViewController.xib in Resources */, B169329B20F39E6300746532 /* Main.storyboard in Resources */, + 32B1FEDB21A46F2C00637127 /* TermsView.xib in Resources */, B1B5578E20EF568D00210D55 /* GroupInviteTableViewCell.xib in Resources */, B1B5582020EF625800210D55 /* SimpleRoomTitleView.xib in Resources */, F083BE061E7009ED00A9B29C /* Riot-Defaults.plist in Resources */, @@ -2868,9 +2875,11 @@ "${BUILT_PRODUCTS_DIR}/OLMKit/OLMKit.framework", "${BUILT_PRODUCTS_DIR}/PiwikTracker/PiwikTracker.framework", "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", + "${BUILT_PRODUCTS_DIR}/Reusable/Reusable.framework", "${PODS_ROOT}/WebRTC/WebRTC.framework", "${BUILT_PRODUCTS_DIR}/cmark/cmark.framework", "${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework", + "${BUILT_PRODUCTS_DIR}/libbase58/libbase58.framework", "${BUILT_PRODUCTS_DIR}/DTCoreText.default-Extension/DTCoreText.framework", "${BUILT_PRODUCTS_DIR}/MatrixKit-AppExtension/MatrixKit.framework", ); @@ -2887,9 +2896,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OLMKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PiwikTracker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reusable.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cmark.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libbase58.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -3023,6 +3034,7 @@ B1B558E920EF768F00210D55 /* RoomSelectedStickerBubbleCell.m in Sources */, B1B558DF20EF768F00210D55 /* RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m in Sources */, F083BE041E7009ED00A9B29C /* Tools.m in Sources */, + 3275FD8C21A5A2C500B9C13D /* TermsView.swift in Sources */, B1B5573D20EE6C4D00210D55 /* WebViewViewController.m in Sources */, B1B5572720EE6C4D00210D55 /* RoomSearchViewController.m in Sources */, F05927C91FDED836009F2A68 /* MXGroup+Riot.m in Sources */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index e34fb4f23..9fad99136 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -107,6 +107,7 @@ "auth_reset_password_error_not_found" = "Your email address does not appear to be associated with a Matrix ID on this Homeserver."; "auth_reset_password_success_message" = "Your password has been reset.\n\nYou have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, re-log in on each device."; "auth_add_email_and_phone_warning" = "Registration with email and phone number at once is not supported yet until the api exists. Only the phone number will be taken into account. You may add your email to your profile in settings."; +"auth_accept_policies" = "Please review and accept the policies of this homeserver:"; // Chat creation "room_creation_title" = "New Chat"; diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index eb8c29a81..cd16fe9d5 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -801,11 +801,10 @@ #pragma mark - MXKAuthInputsViewDelegate -- (void)authInputsView:(MXKAuthInputsView *)authInputsView presentViewController:(UIViewController*)viewControllerToPresent +- (void)authInputsView:(MXKAuthInputsView *)authInputsView presentViewController:(UIViewController*)viewControllerToPresent animated:(BOOL)animated { [self dismissKeyboard]; - - [self presentViewController:viewControllerToPresent animated:YES completion:nil]; + [self presentViewController:viewControllerToPresent animated:animated completion:nil]; } - (void)authInputsViewDidCancelOperation:(MXKAuthInputsView *)authInputsView diff --git a/Riot/Modules/Authentication/Views/AuthInputsView.h b/Riot/Modules/Authentication/Views/AuthInputsView.h index 02fa0edad..e6ce0f9ac 100644 --- a/Riot/Modules/Authentication/Views/AuthInputsView.h +++ b/Riot/Modules/Authentication/Views/AuthInputsView.h @@ -17,6 +17,8 @@ #import +#import "Riot-Swift.h" + @interface AuthInputsView : MXKAuthInputsView @property (weak, nonatomic) IBOutlet UITextField *userLoginTextField; @@ -50,6 +52,7 @@ @property (weak, nonatomic) IBOutlet UILabel *messageLabel; @property (weak, nonatomic) IBOutlet MXKAuthenticationRecaptchaWebView *recaptchaWebView; +@property (weak, nonatomic) IBOutlet TermsView *termsView; /** Tell whether some third-party identifiers may be added during the account registration. diff --git a/Riot/Modules/Authentication/Views/AuthInputsView.m b/Riot/Modules/Authentication/Views/AuthInputsView.m index bee542243..6f27de631 100644 --- a/Riot/Modules/Authentication/Views/AuthInputsView.m +++ b/Riot/Modules/Authentication/Views/AuthInputsView.m @@ -710,6 +710,30 @@ @"auth": @{@"session":currentSession.session, @"username": self.userLoginTextField.text, @"password": self.passWordTextField.text, @"type": kMXLoginFlowTypePassword} }; } + else if ([self isFlowSupported:kMXLoginFlowTypeTerms] && ![self isFlowCompleted:kMXLoginFlowTypeTerms]) + { + NSLog(@"[AuthInputsView] Prepare terms stage"); + + MXWeakify(self); + [self displayTermsView:^{ + MXStrongifyAndReturnIfNil(self); + + NSDictionary *parameters = @{ + @"auth": @{ + @"session":self->currentSession.session, + @"type": kMXLoginFlowTypeTerms + }, + @"username": self.userLoginTextField.text, + @"password": self.passWordTextField.text, + @"bind_msisdn": @([self isFlowCompleted:kMXLoginFlowTypeMSISDN]), + @"bind_email": @([self isFlowCompleted:kMXLoginFlowTypeEmailIdentity]) + }; + callback(parameters, nil); + }]; + + // Async response + return; + } } } @@ -779,6 +803,15 @@ }]; + return; + } + // TODO: avoid that + else if ([self isFlowSupported:kMXLoginFlowTypeTerms] && ![self isFlowCompleted:kMXLoginFlowTypeTerms]) + { + NSLog(@"[AuthInputsView] Prepare a new terms stage"); + + [self prepareParameters:callback]; + return; } } @@ -1158,7 +1191,7 @@ UIBarButtonItem *leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"back_icon"] style:UIBarButtonItemStylePlain target:self action:@selector(dismissCountryPicker)]; phoneNumberCountryPicker.navigationItem.leftBarButtonItem = leftBarButtonItem; - [self.delegate authInputsView:self presentViewController:phoneNumberPickerNavigationController]; + [self.delegate authInputsView:self presentViewController:phoneNumberPickerNavigationController animated:YES]; } } @@ -1259,6 +1292,7 @@ self.messageLabelTopConstraint.constant = 8; self.messageLabel.hidden = YES; self.recaptchaWebView.hidden = YES; + self.termsView.hidden = YES; _currentLastContainer = nil; } @@ -1339,7 +1373,11 @@ { return YES; } - + else if ([flowType isEqualToString:kMXLoginFlowTypeTerms]) + { + return YES; + } + return NO; } @@ -1566,6 +1604,32 @@ [self.delegate authInputsView:self presentAlertController:inputsAlert]; } +- (BOOL)displayTermsView:(dispatch_block_t)onAcceptedCallback +{ + // Extract data + NSDictionary *loginTermsData = currentSession.params[kMXLoginFlowTypeTerms]; + MXLoginTerms *loginTerms; + MXJSONModelSetMXJSONModel(loginTerms, MXLoginTerms.class, loginTermsData); + + if (loginTerms) + { + [self hideInputsContainer]; + + self.messageLabel.hidden = NO; + self.messageLabel.text = NSLocalizedStringFromTable(@"auth_accept_policies", @"Vector", nil); + + self.termsView.hidden = NO; + self.currentLastContainer = self.termsView; + + self.termsView.delegate = self.delegate; + [self.termsView displayTermsWithTerms:loginTerms onAccepted:onAcceptedCallback]; + + return YES; + } + + return NO; +} + #pragma mark - Flow state /** diff --git a/Riot/Modules/Authentication/Views/AuthInputsView.xib b/Riot/Modules/Authentication/Views/AuthInputsView.xib index ef5ee2f58..c60119b25 100644 --- a/Riot/Modules/Authentication/Views/AuthInputsView.xib +++ b/Riot/Modules/Authentication/Views/AuthInputsView.xib @@ -1,11 +1,11 @@ - + - + @@ -247,6 +247,13 @@ + @@ -258,6 +265,7 @@ + @@ -265,11 +273,13 @@ + + @@ -300,6 +310,7 @@ + diff --git a/Riot/Modules/Authentication/Views/TermsView.swift b/Riot/Modules/Authentication/Views/TermsView.swift new file mode 100644 index 000000000..626e0fe11 --- /dev/null +++ b/Riot/Modules/Authentication/Views/TermsView.swift @@ -0,0 +1,208 @@ +/* + Copyright 2018 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 Reusable + +final class TermsView: UIView, NibOwnerLoadable, UITableViewDelegate, UITableViewDataSource { + + @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var acceptButton: UIButton! + + @objc weak var delegate: MXKAuthInputsViewDelegate? + + private var acceptedCallback: (()->Void)? + + + /// NavigationVC to display a policy content + private var navigationController: RiotNavigationController? + + /// The list of policies to be accepted by the end user + private var policies: [MXLoginPolicyData] = [] + + /// Policies already accepted by the end user + /// Combined with `policies`, this is the view model for `tableView` + private var acceptedPolicies: Set = [] + + /// The index of the policy being displayed fullscreen within `navigationController` + private var displayedPolicyIndex: Int? + + + // MARK: - Setup + + convenience init() { + self.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + loadNibContent() + commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + loadNibContent() + commonInit() + } + + private func commonInit() { + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.register(TableViewCellWithCheckBoxAndLabel.nib(), forCellReuseIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier()) + + acceptButton.layer.cornerRadius = 5 + acceptButton.clipsToBounds = true + acceptButton.setTitle(NSLocalizedString("accept", tableName: "Vector", comment: ""), for: .normal) + acceptButton.setTitle(NSLocalizedString("accept", tableName: "Vector", comment: ""), for: .highlighted) + + customizeViewRendering() + } + + func customizeViewRendering() { + acceptButton.backgroundColor = kRiotColorGreen + } + + + // MARK: - Public + + /// Display a list of policies the end user must accept. + /// + /// - Parameters: + /// - terms: Terms data sent by the homeserver. + /// - onAccepted: block called when the user has accepted all of them. + @objc func displayTerms(terms: MXLoginTerms, onAccepted: @escaping () -> Void) { + + acceptedCallback = onAccepted + + let lang: String? = Bundle.mxk_language() + + policies = terms.policiesData(forLanguage: lang, defaultLanguage: "en") + acceptedPolicies.removeAll() + + reload() + } + + + // MARK: - Private + + private func reload() { + tableView.reloadData() + + // Enable the button only if the user has accepted all policies + acceptButton.isEnabled = (policies.count == acceptedPolicies.count) + acceptButton.alpha = acceptButton.isEnabled ? 1 : 0.5 + } + + @IBAction func didAcceptButtonTapped(_ sender: Any) { + if policies.count == acceptedPolicies.count { + acceptedCallback?() + } + } + + + // MARK: - UITableViewDataSource + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return policies.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier(), for: indexPath) as! TableViewCellWithCheckBoxAndLabel + + let policy = policies[indexPath.row] + let accepted = acceptedPolicies .contains(indexPath.row) + + cell.label.text = policy.name + cell.isEnabled = accepted + cell.accessoryType = .disclosureIndicator + + + let gesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapCheckbox)) + gesture.numberOfTapsRequired = 1 + gesture.numberOfTouchesRequired = 1 + + cell.checkBox.tag = indexPath.row + cell.checkBox?.isUserInteractionEnabled = true + cell.checkBox?.addGestureRecognizer(gesture) + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + self.displayPolicy(policyIndex: indexPath.row) + } + + @objc private func didTapCheckbox(sender: UITapGestureRecognizer) { + + if let policyIndex = sender.view?.tag { + + if acceptedPolicies.contains(policyIndex) { + acceptedPolicies.remove(policyIndex) + } + else { + acceptedPolicies.insert(policyIndex) + } + + reload() + } + } + + + // MARK: - Policy content display + + func displayPolicy(policyIndex: Int) { + + displayedPolicyIndex = policyIndex + let policy = policies[policyIndex] + + // 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 + + let rightBarButtonItem: UIBarButtonItem = UIBarButtonItem(title: NSLocalizedString("accept", tableName: "Vector", comment: ""), style: .plain, target: self, action: #selector(didAcceptPolicy)) + webViewViewController.navigationItem.rightBarButtonItem = rightBarButtonItem + + navigationController = RiotNavigationController() + delegate?.authInputsView!(nil, present: navigationController, animated: true) + navigationController?.pushViewController(webViewViewController, animated: false) + } + + @objc private func didTapCancelOnPolicyScreen() { + removePolicyScreen() + } + + @objc private func didAcceptPolicy() { + acceptedPolicies.insert(displayedPolicyIndex!) + + removePolicyScreen() + reload() + } + + private func removePolicyScreen() { + displayedPolicyIndex = nil + + navigationController?.dismiss(animated: false) + navigationController = nil + } +} diff --git a/Riot/Modules/Authentication/Views/TermsView.xib b/Riot/Modules/Authentication/Views/TermsView.xib new file mode 100644 index 000000000..c947e9bba --- /dev/null +++ b/Riot/Modules/Authentication/Views/TermsView.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 59026bb56..9dd6fab82 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -3,4 +3,9 @@ // @import MatrixSDK; +@import MatrixKit; + #import "WebViewViewController.h" +#import "RiotNavigationController.h" +#import "RiotDesignValues.h" +#import "TableViewCellWithCheckBoxAndLabel.h"