diff --git a/src/App/App.csproj b/src/App/App.csproj index 1ffdf334a..a904f0ba6 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -97,6 +97,7 @@ + @@ -110,6 +111,7 @@ + diff --git a/src/App/Constants.cs b/src/App/Constants.cs index 6d5bb8e18..19ecb9b71 100644 --- a/src/App/Constants.cs +++ b/src/App/Constants.cs @@ -18,5 +18,8 @@ public const string PushInitialPromptShown = "push:initialPromptShown"; public const string PushLastRegistrationDate = "push:lastRegistrationDate"; + + public const string ExtensionStarted = "extension:started"; + public const string ExtensionActivated = "extension:activated"; } } diff --git a/src/App/Models/Page/AppExtensionPageModel.cs b/src/App/Models/Page/AppExtensionPageModel.cs new file mode 100644 index 000000000..956fd1a69 --- /dev/null +++ b/src/App/Models/Page/AppExtensionPageModel.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel; +using Plugin.Settings.Abstractions; + +namespace Bit.App.Models.Page +{ + public class AppExtensionPageModel : INotifyPropertyChanged + { + private readonly ISettings _settings; + + public AppExtensionPageModel(ISettings settings) + { + _settings = settings; + } + + public event PropertyChangedEventHandler PropertyChanged; + + public bool Started + { + get { return _settings.GetValueOrDefault(Constants.ExtensionStarted, false); } + set + { + _settings.AddOrUpdateValue(Constants.ExtensionStarted, true); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Started))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(NotStarted))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(StartedAndNotActivated))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(StartedAndActivated))); + } + } + + public bool Activated + { + get { return _settings.GetValueOrDefault(Constants.ExtensionActivated, false); } + set + { + _settings.AddOrUpdateValue(Constants.ExtensionActivated, value); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Activated))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(StartedAndNotActivated))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(StartedAndActivated))); + } + } + + public bool NotStarted => !Started; + public bool StartedAndNotActivated => Started && !Activated; + public bool StartedAndActivated => Started && Activated; + } +} diff --git a/src/App/Pages/Tools/ToolsExtensionPage.cs b/src/App/Pages/Tools/ToolsExtensionPage.cs new file mode 100644 index 000000000..1541a1f73 --- /dev/null +++ b/src/App/Pages/Tools/ToolsExtensionPage.cs @@ -0,0 +1,197 @@ +using System; +using System.Threading.Tasks; +using Acr.UserDialogs; +using Bit.App.Controls; +using Bit.App.Models.Page; +using Plugin.Settings.Abstractions; +using Xamarin.Forms; +using XLabs.Ioc; + +namespace Bit.App.Pages +{ + public class ToolsExtensionPage : ExtendedContentPage + { + private readonly IUserDialogs _userDialogs; + private readonly ISettings _settings; + + public ToolsExtensionPage() + { + _userDialogs = Resolver.Resolve(); + _settings = Resolver.Resolve(); + Model = new AppExtensionPageModel(_settings); + + Init(); + } + + public AppExtensionPageModel Model { get; private set; } + + public void Init() + { + // Not Started + + var notStartedLabel = new Label + { + Text = "Get instant access to your passwords!", + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap, + FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) + }; + + var notStartedSublabel = new Label + { + Text = "To turn on bitwarden in Safari and other apps, tap \"more\" on the second row of the menu.", + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap + }; + + var notStartedImage = new Image + { + Source = "", + VerticalOptions = LayoutOptions.CenterAndExpand, + HorizontalOptions = LayoutOptions.Center + }; + + var notStartedButton = new Button + { + Text = "Enable App Extension", + Command = new Command(() => ActivateExtension()), + VerticalOptions = LayoutOptions.End, + HorizontalOptions = LayoutOptions.Fill, + Style = (Style)Application.Current.Resources["btn-primary"] + }; + + var notStartedStackLayout = new StackLayout + { + Orientation = StackOrientation.Vertical, + Spacing = 20, + Padding = new Thickness(30, 40), + Children = { notStartedLabel, notStartedSublabel, notStartedImage, notStartedButton } + }; + + notStartedStackLayout.SetBinding(IsVisibleProperty, m => m.NotStarted); + + // Not Activated + + var notActivatedLabel = new Label + { + Text = "Almost done!", + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap, + FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) + }; + + var notActivatedSublabel = new Label + { + Text = "Tap the bitwarden icon in the menu to launch the extension.", + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap + }; + + var notActivatedImage = new Image + { + Source = "", + VerticalOptions = LayoutOptions.CenterAndExpand, + HorizontalOptions = LayoutOptions.Center + }; + + var notActivatedButton = new Button + { + Text = "Enable App Extension", + Command = new Command(() => ActivateExtension()), + VerticalOptions = LayoutOptions.End, + HorizontalOptions = LayoutOptions.Fill, + Style = (Style)Application.Current.Resources["btn-primary"] + }; + + var notActivatedStackLayout = new StackLayout + { + Orientation = StackOrientation.Vertical, + Spacing = 20, + Padding = new Thickness(30, 40), + Children = { notActivatedLabel, notActivatedSublabel, notActivatedImage, notActivatedButton } + }; + + notActivatedStackLayout.SetBinding(IsVisibleProperty, m => m.StartedAndNotActivated); + + // Activated + + var activatedLabel = new Label + { + Text = "You're ready to log in!", + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap, + FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)) + }; + + var activatedSublabel = new Label + { + Text = "In Safari, find bitwarden using the share icon (hint: scroll to the right on the second row of the menu).", + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap + }; + + var activatedImage = new Image + { + Source = "", + VerticalOptions = LayoutOptions.CenterAndExpand, + HorizontalOptions = LayoutOptions.Center + }; + + var activatedButton = new Button + { + Text = "See Supported Apps", + Command = new Command(() => Device.OpenUri(new Uri("https://bitwarden.com"))), + VerticalOptions = LayoutOptions.End, + HorizontalOptions = LayoutOptions.Fill, + Style = (Style)Application.Current.Resources["btn-primary"] + }; + + var activatedStackLayout = new StackLayout + { + Orientation = StackOrientation.Vertical, + Spacing = 20, + Padding = new Thickness(30, 40), + Children = { activatedLabel, activatedSublabel, activatedImage, activatedButton } + }; + + activatedStackLayout.SetBinding(IsVisibleProperty, m => m.StartedAndActivated); + + var stackLayout = new StackLayout + { + Children = { notStartedStackLayout, notActivatedStackLayout, activatedStackLayout } + }; + + if(Device.OS == TargetPlatform.iOS) + { + ToolbarItems.Add(new DismissModalToolBarItem(this, "Close")); + } + + Title = "App Extension"; + Content = stackLayout; + BindingContext = Model; + + MessagingCenter.Subscribe(Application.Current, "EnabledAppExtension", (sender, enabled) => + { + Model.Started = true; + Model.Activated = enabled; + }); + } + + private void ActivateExtension() + { + MessagingCenter.Send(Application.Current, "ShowAppExtension"); + } + } +} diff --git a/src/App/Pages/Tools/ToolsPage.cs b/src/App/Pages/Tools/ToolsPage.cs index 83e831513..24fd77596 100644 --- a/src/App/Pages/Tools/ToolsPage.cs +++ b/src/App/Pages/Tools/ToolsPage.cs @@ -4,7 +4,6 @@ using Acr.UserDialogs; using Bit.App.Abstractions; using Bit.App.Controls; using Bit.App.Resources; -using Plugin.Connectivity.Abstractions; using Xamarin.Forms; using XLabs.Ioc; @@ -26,6 +25,7 @@ namespace Bit.App.Pages var generatorCell = new ToolsViewCell("Password Generator", "Automatically generate strong, unique passwords for your logins.", "refresh"); generatorCell.Tapped += GeneratorCell_Tapped; var extensionCell = new ToolsViewCell("bitwarden App Extension", "Use bitwarden in Safari and other apps to auto-fill your logins.", "upload"); + extensionCell.Tapped += ExtensionCell_Tapped; var webCell = new ToolsViewCell("bitwarden Web Vault", "Manage your logins from any web browser with the bitwarden web vault.", "globe"); webCell.Tapped += WebCell_Tapped; var importCell = new ToolsViewCell("Import Logins", "Quickly bulk import your logins from other password management apps.", "cloudup"); @@ -58,6 +58,11 @@ namespace Bit.App.Pages Content = table; } + private void ExtensionCell_Tapped(object sender, EventArgs e) + { + Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsExtensionPage())); + } + private void GeneratorCell_Tapped(object sender, EventArgs e) { Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsPasswordGeneratorPage())); diff --git a/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs b/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs index 8d020c881..c49f8afc7 100644 --- a/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs +++ b/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs @@ -87,7 +87,7 @@ namespace Bit.App.Pages { table.RowHeight = -1; table.EstimatedRowHeight = 44; - ToolbarItems.Add(new DismissModalToolBarItem(this, "Cancel")); + ToolbarItems.Add(new DismissModalToolBarItem(this, "Close")); } var stackLayout = new StackLayout diff --git a/src/iOS.Extension/Info.plist b/src/iOS.Extension/Info.plist index 7c6083377..aa62481d7 100644 --- a/src/iOS.Extension/Info.plist +++ b/src/iOS.Extension/Info.plist @@ -42,6 +42,7 @@ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.change-password-action" || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.fill-webview-action" || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.fill-browser-action" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.8bit.bitwarden.extension-setup" ).@count == $extensionItem.attachments.@count ).@count == 1 NSExtensionPointName diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 94e69436e..e18823e2b 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -54,6 +54,22 @@ namespace Bit.iOS UINavigationBar.Appearance.ShadowImage = new UIImage(); UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + MessagingCenter.Subscribe(Xamarin.Forms.Application.Current, "ShowAppExtension", (sender) => + { + var itemProvider = new NSItemProvider(new NSDictionary(), "com.8bit.bitwarden.extension-setup"); + var extensionItem = new NSExtensionItem(); + extensionItem.Attachments = new NSItemProvider[] { itemProvider }; + var activityViewController = new UIActivityViewController(new NSExtensionItem[] { extensionItem }, null); + activityViewController.CompletionHandler = (activityType, completed) => + { + MessagingCenter.Send(Xamarin.Forms.Application.Current, "EnabledAppExtension", + completed && activityType == "com.8bit.bitwarden.find-login-action-extension"); + }; + + UIApplication.SharedApplication.KeyWindow.RootViewController.ModalViewController + .PresentViewController(activityViewController, true, null); + }); + return base.FinishedLaunching(app, options); }