diff --git a/src/Android/Services/GoogleAnalyticsService.cs b/src/Android/Services/GoogleAnalyticsService.cs index 8627d0c35..09b4adf84 100644 --- a/src/Android/Services/GoogleAnalyticsService.cs +++ b/src/Android/Services/GoogleAnalyticsService.cs @@ -41,6 +41,11 @@ namespace Bit.Android.Services TrackEvent("AutofillService", eventName, label); } + public void TrackExtensionEvent(string eventName, string label = null) + { + TrackExtensionEvent(eventName, label); + } + public void TrackEvent(string category, string eventName, string label = null) { var builder = new HitBuilders.EventBuilder(); diff --git a/src/App/Abstractions/Services/IGoogleAnalyticsService.cs b/src/App/Abstractions/Services/IGoogleAnalyticsService.cs index 9738a724d..f08159f99 100644 --- a/src/App/Abstractions/Services/IGoogleAnalyticsService.cs +++ b/src/App/Abstractions/Services/IGoogleAnalyticsService.cs @@ -7,6 +7,7 @@ namespace Bit.App.Abstractions void TrackPage(string pageName); void TrackAppEvent(string eventName, string label = null); void TrackExtensionEvent(string eventName, string label = null); + void TrackAutofillExtensionEvent(string eventName, string label = null); void TrackEvent(string category, string eventName, string label = null); void TrackException(string message, bool fatal); void Dispatch(Action completionHandler = null); diff --git a/src/App/Services/NoopGoogleAnalyticsService.cs b/src/App/Services/NoopGoogleAnalyticsService.cs index ae00d6d97..3c3362136 100644 --- a/src/App/Services/NoopGoogleAnalyticsService.cs +++ b/src/App/Services/NoopGoogleAnalyticsService.cs @@ -13,6 +13,10 @@ namespace Bit.Android.Services { } + public void TrackAutofillExtensionEvent(string eventName, string label = null) + { + } + public void TrackEvent(string category, string eventName, string label = null) { } diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index ec1a57ab3..c532f9723 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -4,13 +4,16 @@ using Bit.App.Repositories; using Bit.App.Resources; using Bit.App.Services; using Bit.iOS.Autofill.Models; +using Bit.iOS.Core; using Bit.iOS.Core.Services; +using Bit.iOS.Core.Utilities; using Foundation; using Plugin.Connectivity; using Plugin.Fingerprint; using Plugin.Settings.Abstractions; using SimpleInjector; using System; +using System.Diagnostics; using UIKit; using XLabs.Ioc; using XLabs.Ioc.SimpleInjectorContainer; @@ -19,7 +22,9 @@ namespace Bit.iOS.Autofill { public partial class CredentialProviderViewController : ASCredentialProviderViewController { - private Context _context = new Context(); + private Context _context = new Context(); + private bool _setupHockeyApp = false; + private IGoogleAnalyticsService _googleAnalyticsService; public CredentialProviderViewController (IntPtr handle) : base (handle) { @@ -31,14 +36,42 @@ namespace Bit.iOS.Autofill SetCulture(); base.ViewDidLoad(); _context.ExtContext = ExtensionContext; - - // TODO: HockeyApp + _googleAnalyticsService = Resolver.Resolve(); + + if (!_setupHockeyApp) + { + var appIdService = Resolver.Resolve(); + var crashManagerDelegate = new HockeyAppCrashManagerDelegate(appIdService, Resolver.Resolve()); + var manager = HockeyApp.iOS.BITHockeyManager.SharedHockeyManager; + manager.Configure("51f96ae568ba45f699a18ad9f63046c3", crashManagerDelegate); + manager.CrashManager.CrashManagerStatus = HockeyApp.iOS.BITCrashManagerStatus.AutoSend; + manager.UserId = appIdService.AppId; + manager.StartManager(); + manager.Authenticator.AuthenticateInstallation(); + _setupHockeyApp = true; + } } public override void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers) { - System.Diagnostics.Debug.WriteLine("AUTOFILL Got identifiers " + serviceIdentifiers.Length); + _context.ServiceIdentifiers = serviceIdentifiers; + _context.UrlString = serviceIdentifiers[0].Identifier; base.PrepareCredentialList(serviceIdentifiers); + + var authService = Resolver.Resolve(); + if (!authService.IsAuthenticated) + { + var alert = Dialogs.CreateAlert(null, AppResources.MustLogInMainApp, AppResources.Ok, (a) => + { + CompleteRequest(); + }); + PresentViewController(alert, true, null); + return; + } + + + + PerformSegue("loginListSegue", this); } public override void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) @@ -54,6 +87,55 @@ namespace Bit.iOS.Autofill public override void PrepareInterfaceForExtensionConfiguration() { base.PrepareInterfaceForExtensionConfiguration(); + } + + public void CompleteRequest(string username = null, string password = null, string totp = null) + { + if(string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password)) { + _googleAnalyticsService.TrackAutofillExtensionEvent("Canceled"); + var err = new NSError(new NSString("ASExtensionErrorDomain"), + Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null); + _googleAnalyticsService.Dispatch(() => + { + NSRunLoop.Main.BeginInvokeOnMainThread(() => + { + ExtensionContext.CancelRequest(err); + }); + }); + return; + } + + if(!string.IsNullOrWhiteSpace(totp)) + { + UIPasteboard.General.String = totp; + } + + _googleAnalyticsService.TrackAutofillExtensionEvent("AutoFilled"); + var cred = new ASPasswordCredential(username, password); + _googleAnalyticsService.Dispatch(() => + { + NSRunLoop.Main.BeginInvokeOnMainThread(() => + { + ExtensionContext.CompleteRequest(cred, null); + }); + }); + } + + + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + var navController = segue.DestinationViewController as UINavigationController; + if (navController != null) + { + var listLoginController = navController.TopViewController as LoginListViewController; + + if (listLoginController != null) + { + listLoginController.Context = _context; + listLoginController.CPViewController = this; + } + } } private void SetIoc() diff --git a/src/iOS.Autofill/CredentialProviderViewController.designer.cs b/src/iOS.Autofill/CredentialProviderViewController.designer.cs index 9b9e064c1..8cc45f3a4 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.designer.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.designer.cs @@ -14,14 +14,6 @@ namespace Bit.iOS.Autofill [Register ("CredentialProviderViewController")] partial class CredentialProviderViewController { - [Action ("cancel:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void cancel (UIKit.UIBarButtonItem sender); - - [Action ("passwordSelected:")] - [GeneratedCode ("iOS Designer", "1.0")] - partial void passwordSelected (UIKit.UIButton sender); - void ReleaseDesignerOutlets () { } diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs new file mode 100644 index 000000000..743b03edc --- /dev/null +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Bit.App.Abstractions; +using Bit.iOS.Autofill.Models; +using Foundation; +using UIKit; +using XLabs.Ioc; +using Plugin.Settings.Abstractions; +using Bit.iOS.Core.Utilities; +using System.Threading.Tasks; +using Bit.iOS.Core; +using MobileCoreServices; +using Bit.iOS.Core.Controllers; +using Bit.App.Resources; +using Bit.App.Models; +using Bit.App.Utilities; +using Bit.iOS.Core.Models; + +namespace Bit.iOS.Autofill +{ + public partial class LoginListViewController : ExtendedUITableViewController + { + public LoginListViewController(IntPtr handle) : base(handle) + { } + + public Context Context { get; set; } + public CredentialProviderViewController CPViewController { get; set; } + + public override void ViewWillAppear(bool animated) + { + UINavigationBar.Appearance.ShadowImage = new UIImage(); + UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + base.ViewWillAppear(animated); + } + + public async override void ViewDidLoad() + { + base.ViewDidLoad(); + NavItem.Title = AppResources.Items; + CancelBarButton.Title = AppResources.Cancel; + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 44; + TableView.Source = new TableSource(this); + await ((TableSource)TableView.Source).LoadItemsAsync(); + } + + partial void CancelBarButton_Activated(UIBarButtonItem sender) + { + CPViewController.CompleteRequest(null, null, null); + } + + partial void AddBarButton_Activated(UIBarButtonItem sender) + { + PerformSegue("loginAddSegue", this); + } + + public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + { + var navController = segue.DestinationViewController as UINavigationController; + if(navController != null) + { + /* + var addLoginController = navController.TopViewController as LoginAddViewController; + if(addLoginController != null) + { + addLoginController.Context = Context; + addLoginController.LoginListController = this; + } + */ + } + } + + public void DismissModal() + { + DismissViewController(true, async () => + { + await ((TableSource)TableView.Source).LoadItemsAsync(); + TableView.ReloadData(); + }); + } + + public class TableSource : UITableViewSource + { + private const string CellIdentifier = "TableCell"; + + private IEnumerable _tableItems = new List(); + private Context _context; + private LoginListViewController _controller; + private ICipherService _cipherService; + private ISettings _settings; + private bool _accessPremium; + + public TableSource(LoginListViewController controller) + { + _context = controller.Context; + _controller = controller; + _accessPremium = Helpers.CanAccessPremium(); + _cipherService = Resolver.Resolve(); + _settings = Resolver.Resolve(); + } + + public async Task LoadItemsAsync() + { + var combinedLogins = new List(); + + var logins = await _cipherService.GetAllAsync(_context.UrlString); + if(logins?.Item1 != null) + { + combinedLogins.AddRange(logins.Item1); + } + if(logins?.Item2 != null) + { + combinedLogins.AddRange(logins.Item2); + } + + _tableItems = combinedLogins.Select(s => new CipherViewModel(s)) + .OrderBy(s => s.Name) + .ThenBy(s => s.Username) + .ToList() ?? new List(); + } + + public IEnumerable TableItems { get; set; } + + public override nint RowsInSection(UITableView tableview, nint section) + { + return _tableItems == null || _tableItems.Count() == 0 ? 1 : _tableItems.Count(); + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + if(_tableItems == null || _tableItems.Count() == 0) + { + var noDataCell = new UITableViewCell(UITableViewCellStyle.Default, "NoDataCell"); + noDataCell.TextLabel.Text = AppResources.NoItemsTap; + noDataCell.TextLabel.TextAlignment = UITextAlignment.Center; + noDataCell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap; + noDataCell.TextLabel.Lines = 0; + return noDataCell; + } + + var cell = tableView.DequeueReusableCell(CellIdentifier); + + // if there are no cells to reuse, create a new one + if(cell == null) + { + Debug.WriteLine("BW Log, Make new cell for list."); + cell = new UITableViewCell(UITableViewCellStyle.Subtitle, CellIdentifier); + cell.DetailTextLabel.TextColor = cell.DetailTextLabel.TintColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); + } + return cell; + } + + public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath) + { + if(_tableItems == null || _tableItems.Count() == 0 || cell == null) + { + return; + } + + var item = _tableItems.ElementAt(indexPath.Row); + cell.TextLabel.Text = item.Name; + cell.DetailTextLabel.Text = item.Username; + } + + public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + tableView.DeselectRow(indexPath, true); + tableView.EndEditing(true); + + if(_tableItems == null || _tableItems.Count() == 0) + { + _controller.PerformSegue("loginAddSegue", this); + return; + } + + var item = _tableItems.ElementAt(indexPath.Row); + if(item == null) + { + _controller.CPViewController.CompleteRequest(null, null, null); + return; + } + + if(!string.IsNullOrWhiteSpace(item.Password)) + { + string totp = null; + if(!_settings.GetValueOrDefault(App.Constants.SettingDisableTotpCopy, false)) + { + totp = GetTotp(item); + } + + _controller.CPViewController.CompleteRequest(item.Username, item.Password, totp); + } + else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) || + !string.IsNullOrWhiteSpace(item.Totp.Value)) + { + var sheet = Dialogs.CreateActionSheet(item.Name, _controller); + if(!string.IsNullOrWhiteSpace(item.Username)) + { + sheet.AddAction(UIAlertAction.Create(AppResources.CopyUsername, UIAlertActionStyle.Default, a => + { + UIPasteboard clipboard = UIPasteboard.General; + clipboard.String = item.Username; + var alert = Dialogs.CreateMessageAlert(AppResources.CopyUsername); + _controller.PresentViewController(alert, true, () => + { + _controller.DismissViewController(true, null); + }); + })); + } + + if(!string.IsNullOrWhiteSpace(item.Password)) + { + sheet.AddAction(UIAlertAction.Create(AppResources.CopyPassword, UIAlertActionStyle.Default, a => + { + UIPasteboard clipboard = UIPasteboard.General; + clipboard.String = item.Password; + var alert = Dialogs.CreateMessageAlert(AppResources.CopiedPassword); + _controller.PresentViewController(alert, true, () => + { + _controller.DismissViewController(true, null); + }); + })); + } + + if(!string.IsNullOrWhiteSpace(item.Totp.Value)) + { + sheet.AddAction(UIAlertAction.Create(AppResources.CopyTotp, UIAlertActionStyle.Default, a => + { + var totp = GetTotp(item); + if(string.IsNullOrWhiteSpace(totp)) + { + return; + } + + UIPasteboard clipboard = UIPasteboard.General; + clipboard.String = totp; + var alert = Dialogs.CreateMessageAlert(AppResources.CopiedTotp); + _controller.PresentViewController(alert, true, () => + { + _controller.DismissViewController(true, null); + }); + })); + } + + sheet.AddAction(UIAlertAction.Create(AppResources.Cancel, UIAlertActionStyle.Cancel, null)); + _controller.PresentViewController(sheet, true, null); + } + else + { + var alert = Dialogs.CreateAlert(null, AppResources.NoUsernamePasswordConfigured, AppResources.Ok); + _controller.PresentViewController(alert, true, null); + } + } + + private string GetTotp(CipherViewModel item) + { + string totp = null; + if(_accessPremium) + { + if(item != null && !string.IsNullOrWhiteSpace(item.Totp.Value)) + { + totp = Crypto.Totp(item.Totp.Value); + } + } + + return totp; + } + } + } +} diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs new file mode 100644 index 000000000..3e0636502 --- /dev/null +++ b/src/iOS.Autofill/LoginListViewController.designer.cs @@ -0,0 +1,55 @@ +// WARNING +// +// This file has been generated automatically by Visual Studio from the outlets and +// actions declared in your storyboard file. +// Manual changes to this file will not be maintained. +// +using Foundation; +using System; +using System.CodeDom.Compiler; +using UIKit; + +namespace Bit.iOS.Autofill +{ + [Register ("LoginListViewController")] + partial class LoginListViewController + { + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem AddBarButton { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UIBarButtonItem CancelBarButton { get; set; } + + [Outlet] + [GeneratedCode ("iOS Designer", "1.0")] + UIKit.UINavigationItem NavItem { get; set; } + + [Action ("AddBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); + + [Action ("CancelBarButton_Activated:")] + [GeneratedCode ("iOS Designer", "1.0")] + partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); + + void ReleaseDesignerOutlets () + { + if (AddBarButton != null) { + AddBarButton.Dispose (); + AddBarButton = null; + } + + if (CancelBarButton != null) { + CancelBarButton.Dispose (); + CancelBarButton = null; + } + + if (NavItem != null) { + NavItem.Dispose (); + NavItem = null; + } + } + } +} \ No newline at end of file diff --git a/src/iOS.Autofill/MainInterface.storyboard b/src/iOS.Autofill/MainInterface.storyboard index 15a44a92f..896ff1f0f 100644 --- a/src/iOS.Autofill/MainInterface.storyboard +++ b/src/iOS.Autofill/MainInterface.storyboard @@ -1,57 +1,612 @@ - - - - - + + - - - + - - + + - - - - + + + + + + + + - - - - - - - - - - - - - + - + - - - - - + + + + + + - + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + \ No newline at end of file diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs index cf0722d31..f389b83ac 100644 --- a/src/iOS.Autofill/Models/Context.cs +++ b/src/iOS.Autofill/Models/Context.cs @@ -1,9 +1,12 @@ -using Foundation; +using AuthenticationServices; +using Foundation; namespace Bit.iOS.Autofill.Models { public class Context { public NSExtensionContext ExtContext { get; set; } + public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; } + public string UrlString { get; set; } } } diff --git a/src/iOS.Autofill/iOS.Autofill.csproj b/src/iOS.Autofill/iOS.Autofill.csproj index b410d235c..4ca67e667 100644 --- a/src/iOS.Autofill/iOS.Autofill.csproj +++ b/src/iOS.Autofill/iOS.Autofill.csproj @@ -214,6 +214,10 @@ CredentialProviderViewController.cs + + + LoginListViewController.cs + diff --git a/src/iOS.Extension/Models/CipherViewModel.cs b/src/iOS.Core/Models/CipherViewModel.cs similarity index 98% rename from src/iOS.Extension/Models/CipherViewModel.cs rename to src/iOS.Core/Models/CipherViewModel.cs index be88d08e9..869ddc28f 100644 --- a/src/iOS.Extension/Models/CipherViewModel.cs +++ b/src/iOS.Core/Models/CipherViewModel.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Bit.iOS.Extension.Models +namespace Bit.iOS.Core.Models { public class CipherViewModel { diff --git a/src/iOS.Core/Services/GoogleAnalyticsService.cs b/src/iOS.Core/Services/GoogleAnalyticsService.cs index 9bd61d02b..673cb0395 100644 --- a/src/iOS.Core/Services/GoogleAnalyticsService.cs +++ b/src/iOS.Core/Services/GoogleAnalyticsService.cs @@ -33,6 +33,11 @@ namespace Bit.iOS.Core.Services TrackEvent("Extension", eventName, label); } + public void TrackAutofillExtensionEvent(string eventName, string label = null) + { + TrackEvent("AutofillExtension", eventName, label); + } + public void TrackEvent(string category, string eventName, string label = null) { var dict = DictionaryBuilder.CreateEvent(category, eventName, label, null).Build(); diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index d4beb32b9..0aba39c58 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -38,6 +38,7 @@ + @@ -64,6 +65,7 @@ +