1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-26 12:16:07 +01:00

[EC-259] Added Account Switching to Share extension on iOS (#1971)

* EC-259 Added Account switching on share extension on iOS, also improved performance for this and exception handling

* EC-259 code formatting

* EC-259 Added account switching to Share extension Send view

* EC-259 Fixed navigation on share extension when a forms page is already presented

* EC-259 Fix send text UI update when going from the iOS extension

* EC-259 Improved DateTimeViewModel with helper property to easily setup date and time at the same time and applied on usage
This commit is contained in:
Federico Maccaroni 2022-07-12 14:12:23 -03:00 committed by GitHub
parent d621a5d2f3
commit 292908f53f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1509 additions and 423 deletions

View File

@ -20,6 +20,7 @@ using System.Net;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement; using Bit.App.Utilities.AccountManagement;
using Bit.App.Controls;
#if !FDROID #if !FDROID
using Android.Gms.Security; using Android.Gms.Security;
#endif #endif
@ -69,7 +70,8 @@ namespace Bit.Droid
ServiceContainer.Resolve<IStorageService>("secureStorageService"), ServiceContainer.Resolve<IStorageService>("secureStorageService"),
ServiceContainer.Resolve<IStateService>("stateService"), ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"), ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService")); ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager); ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
} }
#if !FDROID #if !FDROID
@ -160,6 +162,7 @@ namespace Bit.Droid
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService); ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService); ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService); ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
// Push // Push
#if FDROID #if FDROID

View File

@ -129,12 +129,10 @@
<Folder Include="Behaviors\" /> <Folder Include="Behaviors\" />
<Folder Include="Controls\AccountSwitchingOverlay\" /> <Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" /> <Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Controls\CipherViewCell\CipherViewCell.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" /> <EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
</ItemGroup> </ItemGroup>
@ -162,12 +160,6 @@
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Styles\Base.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Resources\AppResources.cs.Designer.cs"> <Compile Update="Resources\AppResources.cs.Designer.cs">
<DependentUpon>AppResources.cs.resx</DependentUpon> <DependentUpon>AppResources.cs.resx</DependentUpon>
@ -422,5 +414,6 @@
<None Remove="Xamarin.CommunityToolkit" /> <None Remove="Xamarin.CommunityToolkit" />
<None Remove="Controls\AccountSwitchingOverlay\" /> <None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" /> <None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -13,7 +13,8 @@ namespace Bit.App.Controls
public AccountViewCellViewModel(AccountView accountView) public AccountViewCellViewModel(AccountView accountView)
{ {
AccountView = accountView; AccountView = accountView;
AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email); AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
} }
public AccountView AccountView public AccountView AccountView

View File

@ -50,7 +50,7 @@ namespace Bit.App.Controls
private Stream Draw() private Stream Draw()
{ {
string chars = null; string chars;
string upperData = null; string upperData = null;
if (string.IsNullOrEmpty(_data)) if (string.IsNullOrEmpty(_data))
@ -71,30 +71,39 @@ namespace Bit.App.Controls
var textColor = Color.White; var textColor = Color.White;
var size = 50; var size = 50;
var bitmap = new SKBitmap( using (var bitmap = new SKBitmap(size * 2,
size * 2,
size * 2, size * 2,
SKImageInfo.PlatformColorType, SKImageInfo.PlatformColorType,
SKAlphaType.Premul); SKAlphaType.Premul))
var canvas = new SKCanvas(bitmap); {
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Transparent); canvas.Clear(SKColors.Transparent);
using (var paint = new SKPaint
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
var circlePaint = new SKPaint
{ {
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter, StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex()) Color = SKColor.Parse(bgColor.ToHex())
}; })
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
using (var circlePaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint); canvas.DrawCircle(midX, midY, radius, circlePaint);
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f; var textSize = midX / 1.3f;
var textPaint = new SKPaint using (var textPaint = new SKPaint
{ {
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
@ -102,12 +111,22 @@ namespace Bit.App.Controls
TextSize = textSize, TextSize = textSize,
TextAlign = SKTextAlign.Center, TextAlign = SKTextAlign.Center,
Typeface = typeface Typeface = typeface
}; })
{
var rect = new SKRect(); var rect = new SKRect();
textPaint.MeasureText(chars, ref rect); textPaint.MeasureText(chars, ref rect);
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint); canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream(); using (var img = SKImage.FromBitmap(bitmap))
{
var data = img.Encode(SKEncodedImageFormat.Png, 100);
return data?.AsStream(true);
}
}
}
}
}
}
} }
private string GetFirstLetters(string data, int charCount) private string GetFirstLetters(string data, int charCount)

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Concurrent;
namespace Bit.App.Controls
{
public interface IAvatarImageSourcePool
{
AvatarImageSource GetOrCreateAvatar(string name, string email);
}
public class AvatarImageSourcePool : IAvatarImageSourcePool
{
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
public AvatarImageSource GetOrCreateAvatar(string name, string email)
{
var key = $"{name}{email}";
if (!_cache.TryGetValue(key, out var avatar))
{
avatar = new AvatarImageSource(name, email);
if (!_cache.TryAdd(key, avatar)
&&
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
{
// if add and get after fails, then something wrong is going on with this method.
throw new InvalidOperationException("Something is wrong creating the avatar image");
}
}
return avatar;
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Grid
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.DateTimePicker"
ColumnDefinitions="*,*">
<controls:ExtendedDatePicker
x:Name="_datePicker"
Grid.Column="0"
NullableDate="{Binding Date, Mode=TwoWay}"
Format="d"
AutomationProperties.IsInAccessibleTree="True" />
<controls:ExtendedTimePicker
x:Name="_timePicker"
Grid.Column="1"
NullableTime="{Binding Time, Mode=TwoWay}"
Format="t"
AutomationProperties.IsInAccessibleTree="True" />
</Grid>

View File

@ -0,0 +1,34 @@
using System.Runtime.CompilerServices;
using Xamarin.CommunityToolkit.UI.Views;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class DateTimePicker : Grid
{
public DateTimePicker()
{
InitializeComponent();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(BindingContext)
&&
BindingContext is DateTimeViewModel dateTimeViewModel)
{
AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName);
AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName);
_datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder;
_timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder;
}
}
}
public class LazyDateTimePicker : LazyView<DateTimePicker>
{
}
}

View File

@ -0,0 +1,70 @@
using System;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class DateTimeViewModel : ExtendedViewModel
{
DateTime? _date;
TimeSpan? _time;
public DateTimeViewModel(string dateName, string timeName)
{
DateName = dateName;
TimeName = timeName;
}
public Action<DateTime?> OnDateChanged { get; set; }
public Action<TimeSpan?> OnTimeChanged { get; set; }
public DateTime? Date
{
get => _date;
set
{
if (SetProperty(ref _date, value))
{
OnDateChanged?.Invoke(value);
}
}
}
public TimeSpan? Time
{
get => _time;
set
{
if (SetProperty(ref _time, value))
{
OnTimeChanged?.Invoke(value);
}
}
}
public string DateName { get; }
public string TimeName { get; }
public string DatePlaceholder { get; set; }
public string TimePlaceholder { get; set; }
public DateTime? DateTime
{
get
{
if (Date.HasValue)
{
if (Time.HasValue)
{
return Date.Value.Add(Time.Value);
}
return Date;
}
return null;
}
set
{
Date = value?.Date;
Time = value?.Date.TimeOfDay;
}
}
}
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage <pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms" xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
@ -303,14 +303,14 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<controls:ExtendedDatePicker <controls:ExtendedDatePicker
NullableDate="{Binding DeletionDate, Mode=TwoWay}" NullableDate="{Binding DeletionDateTimeViewModel.Date, Mode=TwoWay}"
Format="d" Format="d"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionDate}" AutomationProperties.Name="{u:I18n DeletionDate}"
Grid.Column="0" /> Grid.Column="0" />
<controls:ExtendedTimePicker <controls:ExtendedTimePicker
NullableTime="{Binding DeletionTime, Mode=TwoWay}" NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
Format="t" Format="t"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
@ -343,7 +343,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<controls:ExtendedDatePicker <controls:ExtendedDatePicker
NullableDate="{Binding ExpirationDate, Mode=TwoWay}" NullableDate="{Binding ExpirationDateTimeViewModel.Date, Mode=TwoWay}"
PlaceHolder="mm/dd/yyyy" PlaceHolder="mm/dd/yyyy"
Format="d" Format="d"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
@ -351,7 +351,7 @@
AutomationProperties.Name="{u:I18n ExpirationDate}" AutomationProperties.Name="{u:I18n ExpirationDate}"
Grid.Column="0" /> Grid.Column="0" />
<controls:ExtendedTimePicker <controls:ExtendedTimePicker
NullableTime="{Binding ExpirationTime, Mode=TwoWay}" NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
PlaceHolder="--:-- --" PlaceHolder="--:-- --"
Format="t" Format="t"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"

View File

@ -23,7 +23,6 @@ namespace Bit.App.Pages
private AppOptions _appOptions; private AppOptions _appOptions;
private SendAddEditPageViewModel _vm; private SendAddEditPageViewModel _vm;
public Action OnClose { get; set; }
public Action AfterSubmit { get; set; } public Action AfterSubmit { get; set; }
public SendAddEditPage( public SendAddEditPage(
@ -135,16 +134,9 @@ namespace Bit.App.Pages
} }
private async Task CloseAsync() private async Task CloseAsync()
{
if (OnClose is null)
{ {
await Navigation.PopModalAsync(); await Navigation.PopModalAsync();
} }
else
{
OnClose();
}
}
protected override bool OnBackButtonPressed() protected override bool OnBackButtonPressed()
{ {

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
@ -23,7 +24,7 @@ namespace Bit.App.Pages
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly ISendService _sendService; private readonly ISendService _sendService;
private readonly ILogger _logger; private readonly ILogger _logger;
private bool _sendEnabled; private bool _sendEnabled = true;
private bool _canAccessPremium; private bool _canAccessPremium;
private bool _emailVerified; private bool _emailVerified;
private SendView _send; private SendView _send;
@ -33,11 +34,7 @@ namespace Bit.App.Pages
private int _deletionDateTypeSelectedIndex; private int _deletionDateTypeSelectedIndex;
private int _expirationDateTypeSelectedIndex; private int _expirationDateTypeSelectedIndex;
private DateTime _simpleDeletionDateTime; private DateTime _simpleDeletionDateTime;
private DateTime _deletionDate;
private TimeSpan _deletionTime;
private DateTime? _simpleExpirationDateTime; private DateTime? _simpleExpirationDateTime;
private DateTime? _expirationDate;
private TimeSpan? _expirationTime;
private bool _isOverridingPickers; private bool _isOverridingPickers;
private int? _maxAccessCount; private int? _maxAccessCount;
private string[] _additionalSendProperties = new[] private string[] _additionalSendProperties = new[]
@ -89,8 +86,34 @@ namespace Bit.App.Pages
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays), new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom), new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
}; };
DeletionDateTimeViewModel = new DateTimeViewModel(AppResources.DeletionDate, AppResources.DeletionTime);
ExpirationDateTimeViewModel = new DateTimeViewModel(AppResources.ExpirationDate, AppResources.ExpirationTime)
{
OnDateChanged = date =>
{
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Time.HasValue)
{
// auto-set time to current time upon setting date
ExpirationDateTimeViewModel.Time = DateTimeNow().TimeOfDay;
}
},
OnTimeChanged = time =>
{
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Date.HasValue)
{
// auto-set date to current date upon setting time
ExpirationDateTimeViewModel.Date = DateTime.Today;
}
},
DatePlaceholder = "mm/dd/yyyy",
TimePlaceholder = "--:-- --"
};
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger);
} }
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command TogglePasswordCommand { get; set; } public Command TogglePasswordCommand { get; set; }
public Command ToggleOptionsCommand { get; set; } public Command ToggleOptionsCommand { get; set; }
public string SendId { get; set; } public string SendId { get; set; }
@ -126,23 +149,14 @@ namespace Bit.App.Pages
} }
} }
} }
public DateTime DeletionDate
{
get => _deletionDate;
set => SetProperty(ref _deletionDate, value);
}
public TimeSpan DeletionTime
{
get => _deletionTime;
set => SetProperty(ref _deletionTime, value);
}
public bool ShowOptions public bool ShowOptions
{ {
get => _showOptions; get => _showOptions;
set => SetProperty(ref _showOptions, value, set => SetProperty(ref _showOptions, value,
additionalPropertyNames: new[] additionalPropertyNames: new[]
{ {
nameof(OptionsAccessilibityText) nameof(OptionsAccessilibityText),
nameof(OptionsShowHideIcon)
}); });
} }
public int ExpirationDateTypeSelectedIndex public int ExpirationDateTypeSelectedIndex
@ -156,28 +170,7 @@ namespace Bit.App.Pages
} }
} }
} }
public DateTime? ExpirationDate
{
get => _expirationDate;
set
{
if (SetProperty(ref _expirationDate, value))
{
ExpirationDateChanged();
}
}
}
public TimeSpan? ExpirationTime
{
get => _expirationTime;
set
{
if (SetProperty(ref _expirationTime, value))
{
ExpirationTimeChanged();
}
}
}
public int? MaxAccessCount public int? MaxAccessCount
{ {
get => _maxAccessCount; get => _maxAccessCount;
@ -205,7 +198,7 @@ namespace Bit.App.Pages
} }
public string FileName public string FileName
{ {
get => _fileName; get => _fileName ?? AppResources.NoFileChosen;
set set
{ {
if (SetProperty(ref _fileName, value)) if (SetProperty(ref _fileName, value))
@ -240,10 +233,13 @@ namespace Bit.App.Pages
public bool IsFile => Send?.Type == SendType.File; public bool IsFile => Send?.Type == SendType.File;
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6; public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7; public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
public DateTimeViewModel DeletionDateTimeViewModel { get; }
public DateTimeViewModel ExpirationDateTimeViewModel { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected; public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected; public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
public string OptionsShowHideIcon => ShowOptions ? BitwardenIcons.ChevronUp : BitwardenIcons.AngleDown;
public async Task InitAsync() public async Task InitAsync()
{ {
@ -268,10 +264,8 @@ namespace Bit.App.Pages
return false; return false;
} }
Send = await send.DecryptAsync(); Send = await send.DecryptAsync();
DeletionDate = Send.DeletionDate.ToLocalTime(); DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime();
DeletionTime = DeletionDate.TimeOfDay; ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime();
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
ExpirationTime = ExpirationDate?.TimeOfDay;
} }
else else
{ {
@ -280,8 +274,7 @@ namespace Bit.App.Pages
{ {
Type = Type.GetValueOrDefault(defaultType), Type = Type.GetValueOrDefault(defaultType),
}; };
_deletionDate = DateTimeNow().AddDays(7); DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7);
_deletionTime = DeletionDate.TimeOfDay;
DeletionDateTypeSelectedIndex = 4; DeletionDateTypeSelectedIndex = 4;
ExpirationDateTypeSelectedIndex = 0; ExpirationDateTypeSelectedIndex = 0;
} }
@ -305,23 +298,22 @@ namespace Bit.App.Pages
public void ClearExpirationDate() public void ClearExpirationDate()
{ {
_isOverridingPickers = true; _isOverridingPickers = true;
ExpirationDate = null; ExpirationDateTimeViewModel.DateTime = null;
ExpirationTime = null;
_isOverridingPickers = false; _isOverridingPickers = false;
} }
private void UpdateSendData() private void UpdateSendData()
{ {
// filename // filename
if (Send.File != null && FileName != null) if (Send.File != null && _fileName != null)
{ {
Send.File.FileName = FileName; Send.File.FileName = _fileName;
} }
// deletion date // deletion date
if (ShowDeletionCustomPickers) if (ShowDeletionCustomPickers)
{ {
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime(); Send.DeletionDate = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime();
} }
else else
{ {
@ -329,9 +321,9 @@ namespace Bit.App.Pages
} }
// expiration date // expiration date
if (ShowExpirationCustomPickers && ExpirationDate.HasValue && ExpirationTime.HasValue) if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue)
{ {
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime(); Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime();
} }
else if (_simpleExpirationDateTime.HasValue) else if (_simpleExpirationDateTime.HasValue)
{ {
@ -484,7 +476,7 @@ namespace Bit.App.Pages
return; return;
} }
if (Page is SendAddEditPage sendPage && sendPage.OnClose != null) if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null)
{ {
sendPage.OnClose(); sendPage.OnClose();
return; return;
@ -625,24 +617,6 @@ namespace Bit.App.Pages
} }
} }
private void ExpirationDateChanged()
{
if (!_isOverridingPickers && !ExpirationTime.HasValue)
{
// auto-set time to current time upon setting date
ExpirationTime = DateTimeNow().TimeOfDay;
}
}
private void ExpirationTimeChanged()
{
if (!_isOverridingPickers && !ExpirationDate.HasValue)
{
// auto-set date to current date upon setting time
ExpirationDate = DateTime.Today;
}
}
private void MaxAccessCountChanged() private void MaxAccessCountChanged()
{ {
Send.MaxAccessCount = _maxAccessCount; Send.MaxAccessCount = _maxAccessCount;
@ -666,5 +640,10 @@ namespace Bit.App.Pages
DateTimeKind.Local DateTimeKind.Local
); );
} }
internal void TriggerSendTextPropertyChanged()
{
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send)));
}
} }
} }

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
x:DataType="pages:SendAddEditPageViewModel"
x:Class="Bit.App.Pages.SendAddOnlyOptionsView">
<ContentView.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentView.Resources>
<ContentView.Content>
<StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,10,0,0">
<Label
Text="{u:I18n DeletionDate}"
StyleClass="box-label" />
<Picker
x:Name="_deletionDateTypePicker"
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
ItemDisplayBinding="{Binding Key}"
ios:Picker.UpdateMode="WhenFinished"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionTime}" />
<controls:LazyDateTimePicker
x:Name="_lazyDeletionDateTimePicker"
BindingContext="{Binding DeletionDateTimeViewModel}"
IsVisible="{Binding ShowDeletionCustomPickers}"
Margin="0,5,0,0" />
<Label
Text="{u:I18n DeletionDateInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout StyleClass="box-row" Margin="0,5,0,0">
<Label
Text="{u:I18n ExpirationDate}"
StyleClass="box-label" />
<Picker
x:Name="_expirationDateTypePicker"
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
ios:Picker.UpdateMode="WhenFinished"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationTime}" />
<controls:LazyDateTimePicker
x:Name="_lazyExpirationDateTimePicker"
BindingContext="{Binding ExpirationDateTimeViewModel}"
IsVisible="{Binding ShowExpirationCustomPickers}"
Margin="0,5,0,0" />
<Label
Text="{u:I18n ExpirationDateInfo}"
StyleClass="box-footer-label"
HorizontalOptions="StartAndExpand"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n MaximumAccessCount}"
StyleClass="box-label" />
<StackLayout
StyleClass="box-row"
Orientation="Horizontal">
<Entry
Text="{Binding MaxAccessCount}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Keyboard="Numeric"
MaxLength="9"
TextChanged="OnMaxAccessCountTextChanged"
HorizontalOptions="FillAndExpand" />
<controls:ExtendedStepper
x:Name="_maxAccessCountStepper"
Value="{Binding MaxAccessCount}"
Maximum="999999999"
IsEnabled="{Binding SendEnabled}"
Margin="10,0,0,0" />
</StackLayout>
<Label
Text="{u:I18n MaximumAccessCountInfo}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n NewPassword}"
StyleClass="box-label" />
<StackLayout Orientation="Horizontal">
<Entry
Text="{Binding NewPassword}"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
HorizontalOptions="FillAndExpand" />
<controls:IconButton
IsEnabled="{Binding SendEnabled}"
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Margin="10,0,0,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</StackLayout>
<Label
Text="{u:I18n PasswordInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n Notes}"
StyleClass="box-label" />
<Editor
x:Name="_notesEditor"
AutoSize="TextChanges"
Text="{Binding Send.Notes}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Margin="0,10,0,5"
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
<Editor.Effects>
<effects:ScrollEnabledEffect />
</Editor.Effects>
</Editor>
<BoxView
StyleClass="box-row-separator" />
<Label
Text="{u:I18n NotesInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,5,0,0">
<Label
Text="{u:I18n HideEmail}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.HideEmail}"
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,5,0,0">
<Label
Text="{u:I18n DisableSend}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Disabled}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
</StackLayout>
</ContentView.Content>
</ContentView>

View File

@ -0,0 +1,91 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Bit.App.Behaviors;
using Xamarin.CommunityToolkit.UI.Views;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SendAddOnlyOptionsView : ContentView
{
public SendAddOnlyOptionsView()
{
InitializeComponent();
}
private SendAddEditPageViewModel ViewModel => BindingContext as SendAddEditPageViewModel;
public void SetMainScrollView(ScrollView scrollView)
{
_notesEditor.Behaviors.Add(new EditorPreventAutoBottomScrollingOnFocusedBehavior { ParentScrollView = scrollView });
}
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
{
if (ViewModel is null)
{
return;
}
if (string.IsNullOrWhiteSpace(e.NewTextValue))
{
ViewModel.MaxAccessCount = null;
_maxAccessCountStepper.Value = 0;
return;
}
// accept only digits
if (!int.TryParse(e.NewTextValue, out int _))
{
((Entry)sender).Text = e.OldTextValue;
}
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(BindingContext)
&&
ViewModel != null)
{
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!_lazyDeletionDateTimePicker.IsLoaded
&&
e.PropertyName == nameof(SendAddEditPageViewModel.ShowDeletionCustomPickers)
&&
ViewModel.ShowDeletionCustomPickers)
{
_lazyDeletionDateTimePicker.LoadViewAsync();
}
if (!_lazyExpirationDateTimePicker.IsLoaded
&&
e.PropertyName == nameof(SendAddEditPageViewModel.ShowExpirationCustomPickers)
&&
ViewModel.ShowExpirationCustomPickers)
{
_lazyExpirationDateTimePicker.LoadViewAsync();
}
}
}
public class SendAddOnlyOptionsLazyView : LazyView<SendAddOnlyOptionsView>
{
public ScrollView MainScrollView { get; set; }
public override async ValueTask LoadViewAsync()
{
await base.LoadViewAsync();
if (Content is SendAddOnlyOptionsView optionsView)
{
optionsView.SetMainScrollView(MainScrollView);
}
}
}
}

View File

@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SendAddOnlyPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
xmlns:effects="clr-namespace:Bit.App.Effects"
x:DataType="pages:SendAddEditPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:SendAddEditPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<!--Order matters here or the avatar's image won't be updated correctly, check iOS CustomNavigationRenderer for more info-->
<controls:ExtendedToolbarItem
x:Name="_accountAvatar"
IconImageSource="{Binding AvatarImageSource}"
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
Order="Primary"
Priority="-2"
UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" />
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<AbsoluteLayout>
<ScrollView
x:Name="_scrollView"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All">
<StackLayout x:Name="_mainContainer" StyleClass="box">
<Frame
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
Padding="10"
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n SendDisabledWarning}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<Frame
IsVisible="{Binding SendOptionsPolicyInEffect}"
Padding="10"
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n SendOptionsPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n Name}"
StyleClass="box-label" />
<Entry
x:Name="_nameEntry"
Text="{Binding Send.Name}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" />
<Label
Text="{u:I18n NameInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
IsVisible="{Binding IsFile}">
<Label
Text="{u:I18n TypeFile}"
StyleClass="box-label" />
<StackLayout
StyleClass="box-row">
<Label
Text="{Binding FileName}"
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
<Label
Margin="0, 5, 0, 0"
Text="{u:I18n MaxFileSize}"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
</StackLayout>
</StackLayout>
<StackLayout
StyleClass="box-row"
IsVisible="{Binding IsText}">
<Label
Text="{u:I18n TypeText}"
StyleClass="box-label" />
<Editor
x:Name="_textEditor"
AutoSize="TextChanges"
Text="{Binding Send.Text.Text}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Margin="{Binding EditorMargins}"
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
<Editor.Behaviors>
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
</Editor.Behaviors>
<Editor.Effects>
<effects:ScrollEnabledEffect />
</Editor.Effects>
</Editor>
<BoxView
StyleClass="box-row-separator" />
<Label
Text="{u:I18n TypeTextInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,10" />
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,10,0,0">
<Label
Text="{u:I18n HideTextByDefault}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Text.Hidden}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch">
<Label
Text="{Binding ShareOnSaveText}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding ShareOnSave}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<StackLayout
Orientation="Horizontal"
Spacing="0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding OptionsAccessilibityText}">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="OptionsHeader_Tapped" />
</StackLayout.GestureRecognizers>
<Label
Text="{u:I18n Options}"
TextColor="{DynamicResource PrimaryColor}"
Margin="0,0,5,0"
AutomationProperties.IsInAccessibleTree="False"/>
<controls:IconLabel
Text="{Binding OptionsShowHideIcon}"
TextColor="{DynamicResource PrimaryColor}"
AutomationProperties.IsInAccessibleTree="False"/>
</StackLayout>
<pages:SendAddOnlyOptionsLazyView x:Name="_lazyOptionsView" IsVisible="{Binding ShowOptions}" />
</StackLayout>
</ScrollView>
<controls:AccountSwitchingOverlayView
x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All"
LongPressAccountEnabled="False"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout>
</pages:BaseContentPage>

View File

@ -0,0 +1,178 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
/// <summary>
/// This is a version of <see cref="SendAddEditPage"/> that is reduced for adding only and adapted
/// for performance for iOS Share extension.
/// </summary>
/// <remarks>
/// This should NOT be used in Android.
/// </remarks>
public partial class SendAddOnlyPage : BaseContentPage
{
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private AppOptions _appOptions;
private SendAddEditPageViewModel _vm;
public Action OnClose { get; set; }
public Action AfterSubmit { get; set; }
public SendAddOnlyPage(
AppOptions appOptions = null,
string sendId = null,
SendType? type = null)
{
if (appOptions?.IosExtension != true)
{
throw new InvalidOperationException(nameof(SendAddOnlyPage) + " is only prepared to be used in iOS share extension");
}
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as SendAddEditPageViewModel;
_vm.Page = this;
_vm.SendId = sendId;
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
if (_vm.IsText)
{
_nameEntry.ReturnType = ReturnType.Next;
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
try
{
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
}
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
await _vm.InitAsync();
if (!await _vm.LoadAsync())
{
await CloseAsync();
return;
}
_accountAvatar?.OnAppearing();
await Device.InvokeOnMainThreadAsync(async () => _vm.AvatarImageSource = await GetAvatarImageSourceAsync());
await HandleCreateRequest();
if (string.IsNullOrWhiteSpace(_vm.Send?.Name))
{
RequestFocus(_nameEntry);
}
AdjustToolbar();
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
await CloseAsync();
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_accountAvatar?.OnDisappearing();
}
private async Task CloseAsync()
{
if (OnClose is null)
{
await Navigation.PopModalAsync();
}
else
{
OnClose();
}
}
private async void Save_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
var submitted = await _vm.SubmitAsync();
if (submitted)
{
AfterSubmit?.Invoke();
}
}
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await CloseAsync();
}
}
private void AdjustToolbar()
{
_saveItem.IsEnabled = _vm.SendEnabled;
}
private Task HandleCreateRequest()
{
if (_appOptions?.CreateSend == null)
{
return Task.CompletedTask;
}
_vm.IsAddFromShare = true;
_vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving;
var name = _appOptions.CreateSend.Item2;
_vm.Send.Name = name;
var type = _appOptions.CreateSend.Item1;
if (type == SendType.File)
{
_vm.FileData = _appOptions.CreateSend.Item3;
_vm.FileName = name;
}
else
{
var text = _appOptions.CreateSend.Item4;
_vm.Send.Text.Text = text;
_vm.TriggerSendTextPropertyChanged();
}
_appOptions.CreateSend = null;
return Task.CompletedTask;
}
void OptionsHeader_Tapped(object sender, EventArgs e)
{
_vm.ToggleOptionsCommand.Execute(null);
if (!_lazyOptionsView.IsLoaded)
{
_lazyOptionsView.MainScrollView = _scrollView;
_lazyOptionsView.LoadViewAsync();
}
}
}
}

View File

@ -19,6 +19,7 @@ namespace Bit.App.Utilities.AccountManagement
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService; private readonly IAuthService _authService;
private readonly ILogger _logger;
Func<AppOptions> _getOptionsFunc; Func<AppOptions> _getOptionsFunc;
private IAccountsManagerHost _accountsManagerHost; private IAccountsManagerHost _accountsManagerHost;
@ -28,7 +29,8 @@ namespace Bit.App.Utilities.AccountManagement
IStorageService secureStorageService, IStorageService secureStorageService,
IStateService stateService, IStateService stateService,
IPlatformUtilsService platformUtilsService, IPlatformUtilsService platformUtilsService,
IAuthService authService) IAuthService authService,
ILogger logger)
{ {
_broadcasterService = broadcasterService; _broadcasterService = broadcasterService;
_vaultTimeoutService = vaultTimeoutService; _vaultTimeoutService = vaultTimeoutService;
@ -36,6 +38,7 @@ namespace Bit.App.Utilities.AccountManagement
_stateService = stateService; _stateService = stateService;
_platformUtilsService = platformUtilsService; _platformUtilsService = platformUtilsService;
_authService = authService; _authService = authService;
_logger = logger;
} }
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true }; private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
@ -108,24 +111,26 @@ namespace Bit.App.Utilities.AccountManagement
} }
private async void OnMessage(Message message) private async void OnMessage(Message message)
{
try
{ {
switch (message.Command) switch (message.Command)
{ {
case AccountsManagerMessageCommands.LOCKED: case AccountsManagerMessageCommands.LOCKED:
Locked(message.Data as Tuple<string, bool>); await Device.InvokeOnMainThreadAsync(() => LockedAsync(message.Data as Tuple<string, bool>));
break; break;
case AccountsManagerMessageCommands.LOCK_VAULT: case AccountsManagerMessageCommands.LOCK_VAULT:
await _vaultTimeoutService.LockAsync(true); await _vaultTimeoutService.LockAsync(true);
break; break;
case AccountsManagerMessageCommands.LOGOUT: case AccountsManagerMessageCommands.LOGOUT:
LogOut(message.Data as Tuple<string, bool, bool>); await Device.InvokeOnMainThreadAsync(() => LogOutAsync(message.Data as Tuple<string, bool, bool>));
break; break;
case AccountsManagerMessageCommands.LOGGED_OUT: case AccountsManagerMessageCommands.LOGGED_OUT:
// Clean up old migrated key if they ever log out. // Clean up old migrated key if they ever log out.
await _secureStorageService.RemoveAsync("oldKey"); await _secureStorageService.RemoveAsync("oldKey");
break; break;
case AccountsManagerMessageCommands.ADD_ACCOUNT: case AccountsManagerMessageCommands.ADD_ACCOUNT:
AddAccount(); await AddAccountAsync();
break; break;
case AccountsManagerMessageCommands.ACCOUNT_ADDED: case AccountsManagerMessageCommands.ACCOUNT_ADDED:
await _accountsManagerHost.UpdateThemeAsync(); await _accountsManagerHost.UpdateThemeAsync();
@ -135,16 +140,17 @@ namespace Bit.App.Utilities.AccountManagement
break; break;
} }
} }
catch (Exception ex)
{
_logger.Exception(ex);
}
}
private void Locked(Tuple<string, bool> extras) private async Task LockedAsync(Tuple<string, bool> extras)
{ {
var userId = extras?.Item1; var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? false; var userInitiated = extras?.Item2 ?? false;
Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated));
}
private async Task LockedAsync(string userId, bool userInitiated)
{
if (!await _stateService.IsActiveAccountAsync(userId)) if (!await _stateService.IsActiveAccountAsync(userId))
{ {
_platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully); _platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully);
@ -163,28 +169,24 @@ namespace Bit.App.Utilities.AccountManagement
await _accountsManagerHost.SetPreviousPageInfoAsync(); await _accountsManagerHost.SetPreviousPageInfoAsync();
Device.BeginInvokeOnMainThread(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric))); await Device.InvokeOnMainThreadAsync(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric)));
} }
private void AddAccount() private async Task AddAccountAsync()
{ {
Device.BeginInvokeOnMainThread(() => await Device.InvokeOnMainThreadAsync(() =>
{ {
Options.HideAccountSwitcher = false; Options.HideAccountSwitcher = false;
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin); _accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
}); });
} }
private void LogOut(Tuple<string, bool, bool> extras) private async Task LogOutAsync(Tuple<string, bool, bool> extras)
{ {
var userId = extras?.Item1; var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? true; var userInitiated = extras?.Item2 ?? true;
var expired = extras?.Item3 ?? false; var expired = extras?.Item3 ?? false;
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired));
}
private async Task LogOutAsync(string userId, bool userInitiated, bool expired)
{
await AppHelpers.LogOutAsync(userId, userInitiated); await AppHelpers.LogOutAsync(userId, userInitiated);
await NavigateOnAccountChangeAsync(); await NavigateOnAccountChangeAsync();
_authService.LogOut(() => _authService.LogOut(() =>

View File

@ -1,20 +1,20 @@
using System; using System;
using UIKit;
using Foundation;
using Bit.iOS.Core.Views;
using Bit.App.Resources;
using Bit.iOS.Core.Utilities;
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Utilities; using Bit.App.Abstractions;
using Bit.Core.Models.Domain;
using Bit.Core.Enums;
using Bit.App.Pages;
using Bit.App.Models; using Bit.App.Models;
using Xamarin.Forms; using Bit.App.Pages;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views;
using Foundation;
using UIKit;
using Xamarin.Forms;
namespace Bit.iOS.Core.Controllers namespace Bit.iOS.Core.Controllers
{ {
@ -39,6 +39,10 @@ namespace Bit.iOS.Core.Controllers
protected bool autofillExtension = false; protected bool autofillExtension = false;
public BaseLockPasswordViewController()
{
}
public BaseLockPasswordViewController(IntPtr handle) public BaseLockPasswordViewController(IntPtr handle)
: base(handle) : base(handle)
{ } { }
@ -168,12 +172,11 @@ namespace Bit.iOS.Core.Controllers
{ {
TableView.BackgroundColor = ThemeHelpers.BackgroundColor; TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
TableView.SeparatorColor = ThemeHelpers.SeparatorColor; TableView.SeparatorColor = ThemeHelpers.SeparatorColor;
}
TableView.RowHeight = UITableView.AutomaticDimension; TableView.RowHeight = UITableView.AutomaticDimension;
TableView.EstimatedRowHeight = 70; TableView.EstimatedRowHeight = 70;
TableView.Source = new TableSource(this); TableView.Source = new TableSource(this);
TableView.AllowsSelection = true; TableView.AllowsSelection = true;
}
base.ViewDidLoad(); base.ViewDidLoad();
@ -191,7 +194,7 @@ namespace Bit.iOS.Core.Controllers
} }
} }
public override async void ViewDidAppear(bool animated) public override void ViewDidAppear(bool animated)
{ {
base.ViewDidAppear(animated); base.ViewDidAppear(animated);
@ -402,28 +405,43 @@ namespace Bit.iOS.Core.Controllers
}); });
} }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
MasterPasswordCell?.Dispose();
MasterPasswordCell = null;
TableView?.Dispose();
}
public class TableSource : ExtendedUITableViewSource public class TableSource : ExtendedUITableViewSource
{ {
private readonly BaseLockPasswordViewController _controller; private readonly WeakReference<BaseLockPasswordViewController> _controller;
public TableSource(BaseLockPasswordViewController controller) public TableSource(BaseLockPasswordViewController controller)
{ {
_controller = controller; _controller = new WeakReference<BaseLockPasswordViewController>(controller);
} }
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{ {
if (!_controller.TryGetTarget(out var controller))
{
return new ExtendedUITableViewCell();
}
if (indexPath.Section == 0) if (indexPath.Section == 0)
{ {
if (indexPath.Row == 0) if (indexPath.Row == 0)
{ {
if (_controller._biometricUnlockOnly) if (controller._biometricUnlockOnly)
{ {
return _controller.BiometricCell; return controller.BiometricCell;
} }
else else
{ {
return _controller.MasterPasswordCell; return controller.MasterPasswordCell;
} }
} }
} }
@ -431,7 +449,7 @@ namespace Bit.iOS.Core.Controllers
{ {
if (indexPath.Row == 0) if (indexPath.Row == 0)
{ {
if (_controller._passwordReprompt) if (controller._passwordReprompt)
{ {
var cell = new ExtendedUITableViewCell(); var cell = new ExtendedUITableViewCell();
cell.TextLabel.TextColor = ThemeHelpers.DangerColor; cell.TextLabel.TextColor = ThemeHelpers.DangerColor;
@ -441,9 +459,9 @@ namespace Bit.iOS.Core.Controllers
cell.TextLabel.Text = AppResources.PasswordConfirmationDesc; cell.TextLabel.Text = AppResources.PasswordConfirmationDesc;
return cell; return cell;
} }
else if (!_controller._biometricUnlockOnly) else if (!controller._biometricUnlockOnly)
{ {
return _controller.BiometricCell; return controller.BiometricCell;
} }
} }
} }
@ -457,8 +475,13 @@ namespace Bit.iOS.Core.Controllers
public override nint NumberOfSections(UITableView tableView) public override nint NumberOfSections(UITableView tableView)
{ {
return (!_controller._biometricUnlockOnly && _controller._biometricLock) || if (!_controller.TryGetTarget(out var controller))
_controller._passwordReprompt {
return 0;
}
return (!controller._biometricUnlockOnly && controller._biometricLock) ||
controller._passwordReprompt
? 2 ? 2
: 1; : 1;
} }
@ -484,13 +507,18 @@ namespace Bit.iOS.Core.Controllers
public override void RowSelected(UITableView tableView, NSIndexPath indexPath) public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{ {
if (!_controller.TryGetTarget(out var controller))
{
return;
}
tableView.DeselectRow(indexPath, true); tableView.DeselectRow(indexPath, true);
tableView.EndEditing(true); tableView.EndEditing(true);
if (indexPath.Row == 0 && if (indexPath.Row == 0 &&
((_controller._biometricUnlockOnly && indexPath.Section == 0) || ((controller._biometricUnlockOnly && indexPath.Section == 0) ||
indexPath.Section == 1)) indexPath.Section == 1))
{ {
var task = _controller.PromptBiometricAsync(); var task = controller.PromptBiometricAsync();
return; return;
} }
var cell = tableView.CellAt(indexPath); var cell = tableView.CellAt(indexPath);

View File

@ -8,6 +8,10 @@ namespace Bit.iOS.Core.Controllers
{ {
public Action DismissModalAction { get; set; } public Action DismissModalAction { get; set; }
public ExtendedUIViewController()
{
}
public ExtendedUIViewController(IntPtr handle) public ExtendedUIViewController(IntPtr handle)
: base(handle) : base(handle)
{ {
@ -28,16 +32,28 @@ namespace Bit.iOS.Core.Controllers
{ {
View.BackgroundColor = ThemeHelpers.BackgroundColor; View.BackgroundColor = ThemeHelpers.BackgroundColor;
} }
if (NavigationController?.NavigationBar != null) UpdateNavigationBarTheme();
}
protected virtual void UpdateNavigationBarTheme()
{ {
NavigationController.NavigationBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor; UpdateNavigationBarTheme(NavigationController?.NavigationBar);
NavigationController.NavigationBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor; }
NavigationController.NavigationBar.TintColor = ThemeHelpers.NavBarTextColor;
NavigationController.NavigationBar.TitleTextAttributes = new UIStringAttributes protected void UpdateNavigationBarTheme(UINavigationBar navBar)
{
if (navBar is null)
{
return;
}
navBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor;
navBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor;
navBar.TintColor = ThemeHelpers.NavBarTextColor;
navBar.TitleTextAttributes = new UIStringAttributes
{ {
ForegroundColor = ThemeHelpers.NavBarTextColor ForegroundColor = ThemeHelpers.NavBarTextColor
}; };
} }
} }
} }
}

View File

@ -184,7 +184,7 @@ namespace Bit.iOS.Core.Controllers
} }
} }
public override async void ViewDidAppear(bool animated) public override void ViewDidAppear(bool animated)
{ {
base.ViewDidAppear(animated); base.ViewDidAppear(animated);

View File

@ -13,9 +13,9 @@ namespace Bit.iOS.Core.Utilities
{ {
const string DEFAULT_SYSTEM_AVATAR_IMAGE = "person.2"; const string DEFAULT_SYSTEM_AVATAR_IMAGE = "person.2";
IStateService _stateService; readonly IStateService _stateService;
IMessagingService _messagingService; readonly IMessagingService _messagingService;
ILogger _logger; readonly ILogger _logger;
public AccountSwitchingOverlayHelper() public AccountSwitchingOverlayHelper()
{ {
@ -34,9 +34,11 @@ namespace Bit.iOS.Core.Utilities
} }
var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
var avatarUIImage = await avatarImageSource.GetNativeImageAsync(); using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync())
{
return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);
} }
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.Exception(ex); _logger.Exception(ex);

View File

@ -2,6 +2,7 @@
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Resources; using Bit.App.Resources;
@ -15,6 +16,7 @@ using Bit.iOS.Core.Services;
using CoreNFC; using CoreNFC;
using Foundation; using Foundation;
using UIKit; using UIKit;
using Xamarin.Forms;
namespace Bit.iOS.Core.Utilities namespace Bit.iOS.Core.Utilities
{ {
@ -26,6 +28,42 @@ namespace Bit.iOS.Core.Utilities
public static string AppGroupId = "group.com.8bit.bitwarden"; public static string AppGroupId = "group.com.8bit.bitwarden";
public static string AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden"; public static string AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden";
public static void InitApp<T>(T rootController,
string clearCipherCacheKey,
NFCNdefReaderSession nfcSession,
out NFCReaderDelegate nfcDelegate,
out IAccountsManager accountsManager)
where T : UIViewController, IAccountsManagerHost
{
Forms.Init();
if (ServiceContainer.RegisteredServices.Count > 0)
{
ServiceContainer.Reset();
}
RegisterLocalServices();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
ServiceContainer.Init(deviceActionService.DeviceUserAgent,
clearCipherCacheKey,
Bit.Core.Constants.iOSAllClearCipherCacheKeys);
InitLogger();
Bootstrap();
var appOptions = new AppOptions { IosExtension = true };
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
AppearanceAdjustments();
nfcDelegate = new Core.NFCReaderDelegate((success, message) =>
messagingService.Send("gotYubiKeyOTP", message));
SubscribeBroadcastReceiver(rootController, nfcSession, nfcDelegate);
accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
accountsManager.Init(() => appOptions, rootController);
}
public static void InitLogger() public static void InitLogger()
{ {
ServiceContainer.Resolve<ILogger>("logger").InitAsync(); ServiceContainer.Resolve<ILogger>("logger").InitAsync();
@ -89,6 +127,7 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService); ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService); ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService); ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
} }
public static void Bootstrap(Func<Task> postBootstrapFunc = null) public static void Bootstrap(Func<Task> postBootstrapFunc = null)
@ -181,7 +220,8 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Resolve<IStorageService>("secureStorageService"), ServiceContainer.Resolve<IStorageService>("secureStorageService"),
ServiceContainer.Resolve<IStateService>("stateService"), ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"), ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService")); ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager); ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
if (postBootstrapFunc != null) if (postBootstrapFunc != null)

View File

@ -0,0 +1,27 @@
// This file has been autogenerated from a class added in the UI designer.
using System;
using UIKit;
namespace Bit.iOS.ShareExtension
{
public partial class ExtensionNavigationController : UINavigationController
{
public ExtensionNavigationController (IntPtr handle) : base (handle)
{
}
public override UIViewController PopViewController(bool animated)
{
TopViewController?.Dispose();
return base.PopViewController(animated);
}
public override void DismissModalViewController(bool animated)
{
ModalViewController?.Dispose();
base.DismissModalViewController(animated);
}
}
}

View File

@ -0,0 +1,20 @@
// WARNING
//
// This file has been generated automatically by Visual Studio to store outlets and
// actions made in the UI designer. If it is removed, they will be lost.
// Manual changes to this file may not be handled correctly.
//
using Foundation;
using System.CodeDom.Compiler;
namespace Bit.iOS.ShareExtension
{
[Register ("ExtensionNavigationController")]
partial class ExtensionNavigationController
{
void ReleaseDesignerOutlets ()
{
}
}
}

View File

@ -7,11 +7,11 @@ using Bit.App.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.iOS.Core;
using Bit.iOS.Core.Controllers; using Bit.iOS.Core.Controllers;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views; using Bit.iOS.Core.Views;
@ -24,16 +24,33 @@ using Xamarin.Forms;
namespace Bit.iOS.ShareExtension namespace Bit.iOS.ShareExtension
{ {
public partial class LoadingViewController : ExtendedUIViewController public partial class LoadingViewController : ExtendedUIViewController, IAccountsManagerHost
{ {
const string STORYBOARD_NAME = "MainInterface";
private Context _context = new Context(); private Context _context = new Context();
private NFCNdefReaderSession _nfcSession = null; private NFCNdefReaderSession _nfcSession = null;
private Core.NFCReaderDelegate _nfcDelegate = null; private Core.NFCReaderDelegate _nfcDelegate = null;
private IAccountsManager _accountsManager;
readonly LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>("stateService"); readonly LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>("stateService");
readonly LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>("vaultTimeoutService"); readonly LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>("vaultTimeoutService");
readonly LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>("deviceActionService");
readonly LazyResolve<IEventService> _eventService = new LazyResolve<IEventService>("eventService"); Lazy<UIStoryboard> _storyboard = new Lazy<UIStoryboard>(() => UIStoryboard.FromName(STORYBOARD_NAME, null));
private App.App _app = null;
private UIViewController _currentModalController;
private bool _presentingOnNavigationPage;
private ExtensionNavigationController ExtNavigationController
{
get
{
NavigationController.PresentationController.Delegate =
new CustomPresentationControllerDelegate(CompleteRequest);
return NavigationController as ExtensionNavigationController;
}
}
public LoadingViewController(IntPtr handle) public LoadingViewController(IntPtr handle)
: base(handle) : base(handle)
@ -41,39 +58,38 @@ namespace Bit.iOS.ShareExtension
public override void ViewDidLoad() public override void ViewDidLoad()
{ {
InitApp(); iOSCoreHelpers.InitApp(this, Bit.Core.Constants.iOSShareExtensionClearCiphersCacheKey,
_nfcSession, out _nfcDelegate, out _accountsManager);
base.ViewDidLoad(); base.ViewDidLoad();
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png"); Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor; View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
_context.ExtensionContext = ExtensionContext; _context.ExtensionContext = ExtensionContext;
_context.ProviderType = GetProviderTypeFromExtensionInputItems();
}
/// <summary>
/// Gets the provider <see cref="UTType"/> given the input items
/// </summary>
private string GetProviderTypeFromExtensionInputItems()
{
foreach (var item in ExtensionContext.InputItems) foreach (var item in ExtensionContext.InputItems)
{ {
var processed = false;
foreach (var itemProvider in item.Attachments) foreach (var itemProvider in item.Attachments)
{ {
if (itemProvider.HasItemConformingTo(UTType.PlainText)) if (itemProvider.HasItemConformingTo(UTType.PlainText))
{ {
_context.ProviderType = UTType.PlainText; return UTType.PlainText;
}
processed = true; if (itemProvider.HasItemConformingTo(UTType.Data))
break;
}
else if (itemProvider.HasItemConformingTo(UTType.Data))
{ {
_context.ProviderType = UTType.Data; return UTType.Data;
processed = true;
break;
} }
} }
if (processed)
{
break;
}
} }
return null;
} }
public override async void ViewDidAppear(bool animated) public override async void ViewDidAppear(bool animated)
@ -84,12 +100,12 @@ namespace Bit.iOS.ShareExtension
{ {
if (!await IsAuthed()) if (!await IsAuthed())
{ {
LaunchHomePage(); await _accountsManager.NavigateOnAccountChangeAsync(false);
return; return;
} }
else if (await IsLocked()) else if (await IsLocked())
{ {
PerformSegue("lockPasswordSegue", this); NavigateToLockViewController();
} }
else else
{ {
@ -102,24 +118,52 @@ namespace Bit.iOS.ShareExtension
} }
} }
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) void NavigateToLockViewController()
{ {
if (segue.DestinationViewController is UINavigationController navController var viewController = _storyboard.Value.InstantiateViewController("lockVC") as LockPasswordViewController;
&& viewController.LoadingController = this;
navController.TopViewController is LockPasswordViewController passwordViewController) viewController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
if (_presentingOnNavigationPage)
{ {
passwordViewController.LoadingController = this; _presentingOnNavigationPage = false;
segue.DestinationViewController.PresentationController.Delegate = DismissViewController(true, () => ExtNavigationController.PushViewController(viewController, true));
new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction); }
else
{
ExtNavigationController.PushViewController(viewController, true);
} }
} }
public void DismissLockAndContinue() public void DismissLockAndContinue()
{ {
Debug.WriteLine("BW Log, Dismissing lock controller."); Debug.WriteLine("BW Log, Dismissing lock controller.");
ClearBeforeNavigating();
DismissViewController(false, () => ContinueOnAsync().FireAndForget()); DismissViewController(false, () => ContinueOnAsync().FireAndForget());
} }
private void DismissAndLaunch(Action pageToLaunch)
{
ClearBeforeNavigating();
DismissViewController(false, pageToLaunch);
}
void ClearBeforeNavigating()
{
_currentModalController?.Dispose();
_currentModalController = null;
if (_storyboard.IsValueCreated)
{
_storyboard.Value.Dispose();
_storyboard = null;
_storyboard = new Lazy<UIStoryboard>(() => UIStoryboard.FromName(STORYBOARD_NAME, null));
}
}
private async Task ContinueOnAsync() private async Task ContinueOnAsync()
{ {
Tuple<SendType, string, byte[], string> createSend = null; Tuple<SendType, string, byte[], string> createSend = null;
@ -140,20 +184,24 @@ namespace Bit.iOS.ShareExtension
CreateSend = createSend, CreateSend = createSend,
CopyInsteadOfShareAfterSaving = true CopyInsteadOfShareAfterSaving = true
}; };
var sendAddEditPage = new SendAddEditPage(appOptions) var sendPage = new SendAddOnlyPage(appOptions)
{ {
OnClose = () => CompleteRequest(), OnClose = () => CompleteRequest(),
AfterSubmit = () => CompleteRequest() AfterSubmit = () => CompleteRequest()
}; };
var app = new App.App(appOptions); SetupAppAndApplyResources(sendPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(sendAddEditPage);
var navigationPage = new NavigationPage(sendAddEditPage); NavigateToPage(sendPage);
var sendAddEditController = navigationPage.CreateViewController(); }
sendAddEditController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(sendAddEditController, true, null); private void NavigateToPage(ContentPage page)
{
var navigationPage = new NavigationPage(page);
_currentModalController = navigationPage.CreateViewController();
_currentModalController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
_presentingOnNavigationPage = true;
PresentViewController(_currentModalController, true, null);
} }
private async Task<(string, byte[])> LoadDataBytesAsync() private async Task<(string, byte[])> LoadDataBytesAsync()
@ -202,31 +250,6 @@ namespace Bit.iOS.ShareExtension
}); });
} }
private void InitApp()
{
// Init Xamarin Forms
Forms.Init();
if (ServiceContainer.RegisteredServices.Count > 0)
{
ServiceContainer.Reset();
}
iOSCoreHelpers.RegisterLocalServices();
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
ServiceContainer.Init(_deviceActionService.Value.DeviceUserAgent,
Bit.Core.Constants.iOSShareExtensionClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys);
iOSCoreHelpers.InitLogger();
iOSCoreHelpers.Bootstrap();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
iOSCoreHelpers.AppearanceAdjustments();
_nfcDelegate = new NFCReaderDelegate((success, message) =>
messagingService.Send("gotYubiKeyOTP", message));
iOSCoreHelpers.SubscribeBroadcastReceiver(this, _nfcSession, _nfcDelegate);
}
private Task<bool> IsLocked() private Task<bool> IsLocked()
{ {
return _vaultTimeoutService.Value.IsLockedAsync(); return _vaultTimeoutService.Value.IsLockedAsync();
@ -244,7 +267,7 @@ namespace Bit.iOS.ShareExtension
if (await IsAuthed()) if (await IsAuthed())
{ {
await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync());
if (_deviceActionService.Value.SystemMajorVersion() >= 12) if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{ {
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
} }
@ -252,83 +275,75 @@ namespace Bit.iOS.ShareExtension
}); });
} }
private App.App SetupAppAndApplyResources(ContentPage page)
{
if (_app is null)
{
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
}
ThemeManager.ApplyResourcesToPage(page);
return _app;
}
private void LaunchHomePage() private void LaunchHomePage()
{ {
var homePage = new HomePage(); var homePage = new HomePage();
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(homePage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(homePage);
if (homePage.BindingContext is HomeViewModel vm) if (homePage.BindingContext is HomeViewModel vm)
{ {
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow()); vm.StartLoginAction = () => DismissAndLaunch(() => LaunchLoginFlow());
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow()); vm.StartRegisterAction = () => DismissAndLaunch(() => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow()); vm.StartEnvironmentAction = () => DismissAndLaunch(() => LaunchEnvironmentFlow());
vm.CloseAction = () => CompleteRequest(); vm.CloseAction = () => CompleteRequest();
} }
var navigationPage = new NavigationPage(homePage); NavigateToPage(homePage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
LogoutIfAuthed(); LogoutIfAuthed();
} }
private void LaunchEnvironmentFlow() private void LaunchEnvironmentFlow()
{ {
var environmentPage = new EnvironmentPage(); var environmentPage = new EnvironmentPage();
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(environmentPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(environmentPage); ThemeManager.ApplyResourcesToPage(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm) if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{ {
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage()); vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
} }
var navigationPage = new NavigationPage(environmentPage); NavigateToPage(environmentPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
} }
private void LaunchRegisterFlow() private void LaunchRegisterFlow()
{ {
var registerPage = new RegisterPage(null); var registerPage = new RegisterPage(null);
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(registerPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(registerPage);
if (registerPage.BindingContext is RegisterPageViewModel vm) if (registerPage.BindingContext is RegisterPageViewModel vm)
{ {
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email)); vm.RegistrationSuccess = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
} }
NavigateToPage(registerPage);
var navigationPage = new NavigationPage(registerPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
} }
private void LaunchLoginFlow(string email = null) private void LaunchLoginFlow(string email = null)
{ {
var loginPage = new LoginPage(email); var loginPage = new LoginPage(email);
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(loginPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
if (loginPage.BindingContext is LoginPageViewModel vm) if (loginPage.BindingContext is LoginPageViewModel vm)
{ {
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false)); vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
vm.LogInSuccessAction = () => DismissLockAndContinue(); vm.LogInSuccessAction = () =>
{
DismissLockAndContinue();
};
vm.CloseAction = () => CompleteRequest(); vm.CloseAction = () => CompleteRequest();
} }
NavigateToPage(loginPage);
var navigationPage = new NavigationPage(loginPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
LogoutIfAuthed(); LogoutIfAuthed();
} }
@ -336,22 +351,16 @@ namespace Bit.iOS.ShareExtension
private void LaunchLoginSsoFlow() private void LaunchLoginSsoFlow()
{ {
var loginPage = new LoginSsoPage(); var loginPage = new LoginSsoPage();
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(loginPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm) if (loginPage.BindingContext is LoginSsoPageViewModel vm)
{ {
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true)); vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(true));
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow());
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
vm.SsoAuthSuccessAction = () => DismissLockAndContinue(); vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
} }
NavigateToPage(loginPage);
var navigationPage = new NavigationPage(loginPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
LogoutIfAuthed(); LogoutIfAuthed();
} }
@ -359,65 +368,97 @@ namespace Bit.iOS.ShareExtension
private void LaunchTwoFactorFlow(bool authingWithSso) private void LaunchTwoFactorFlow(bool authingWithSso)
{ {
var twoFactorPage = new TwoFactorPage(); var twoFactorPage = new TwoFactorPage();
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(twoFactorPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(twoFactorPage);
if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm)
{ {
vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue();
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow());
if (authingWithSso) if (authingWithSso)
{ {
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
} }
else else
{ {
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow()); vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginFlow());
} }
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
} }
NavigateToPage(twoFactorPage);
var navigationPage = new NavigationPage(twoFactorPage);
var twoFactorController = navigationPage.CreateViewController();
twoFactorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(twoFactorController, true, null);
} }
private void LaunchSetPasswordFlow() private void LaunchSetPasswordFlow()
{ {
var setPasswordPage = new SetPasswordPage(); var setPasswordPage = new SetPasswordPage();
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(setPasswordPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(setPasswordPage);
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
{ {
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
vm.SetPasswordSuccessAction = () => DismissLockAndContinue(); vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
} }
NavigateToPage(setPasswordPage);
var navigationPage = new NavigationPage(setPasswordPage);
var setPasswordController = navigationPage.CreateViewController();
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(setPasswordController, true, null);
} }
private void LaunchUpdateTempPasswordFlow() private void LaunchUpdateTempPasswordFlow()
{ {
var updateTempPasswordPage = new UpdateTempPasswordPage(); var updateTempPasswordPage = new UpdateTempPasswordPage();
var app = new App.App(new AppOptions { IosExtension = true }); SetupAppAndApplyResources(updateTempPasswordPage);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(updateTempPasswordPage);
if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm)
{ {
vm.UpdateTempPasswordSuccessAction = () => DismissViewController(false, () => LaunchHomePage()); vm.UpdateTempPasswordSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.LogOutAction = () => DismissViewController(false, () => LaunchHomePage()); vm.LogOutAction = () => DismissAndLaunch(() => LaunchHomePage());
}
NavigateToPage(updateTempPasswordPage);
} }
var navigationPage = new NavigationPage(updateTempPasswordPage); public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null)
var updateTempPasswordController = navigationPage.CreateViewController(); {
updateTempPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; if (ExtNavigationController?.ViewControllers?.Any() ?? false)
PresentViewController(updateTempPasswordController, true, null); {
ExtNavigationController.PopViewController(false);
}
else if (ExtNavigationController?.ModalViewController != null)
{
ExtNavigationController.DismissModalViewController(false);
}
switch (navTarget)
{
case NavigationTarget.HomeLogin:
ExecuteLaunch(LaunchHomePage);
break;
case NavigationTarget.Login:
if (navParams is LoginNavigationParams loginParams)
{
ExecuteLaunch(() => LaunchLoginFlow(loginParams.Email));
}
else
{
ExecuteLaunch(() => LaunchLoginFlow());
}
break;
case NavigationTarget.Lock:
NavigateToLockViewController();
break;
case NavigationTarget.Home:
DismissLockAndContinue();
break;
} }
} }
private void ExecuteLaunch(Action launchAction)
{
if (_presentingOnNavigationPage)
{
DismissAndLaunch(launchAction);
}
else
{
launchAction();
}
}
public Task SetPreviousPageInfoAsync() => Task.CompletedTask;
public Task UpdateThemeAsync() => Task.CompletedTask;
}
} }

View File

@ -1,11 +1,22 @@
using Bit.App.Controls;
using Bit.Core.Utilities;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using System; using System;
using UIKit; using UIKit;
namespace Bit.iOS.ShareExtension namespace Bit.iOS.ShareExtension
{ {
public partial class LockPasswordViewController : Core.Controllers.LockPasswordViewController public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController
{ {
AccountSwitchingOverlayView _accountSwitchingOverlayView;
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
public LockPasswordViewController()
{
BiometricIntegrityKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegrityKey;
DismissModalAction = Cancel;
}
public LockPasswordViewController(IntPtr handle) public LockPasswordViewController(IntPtr handle)
: base(handle) : base(handle)
{ {
@ -17,24 +28,80 @@ namespace Bit.iOS.ShareExtension
public override UINavigationItem BaseNavItem => _navItem; public override UINavigationItem BaseNavItem => _navItem;
public override UIBarButtonItem BaseCancelButton => _cancelButton; public override UIBarButtonItem BaseCancelButton => _cancelButton;
public override UIBarButtonItem BaseSubmitButton => _submitButton; public override UIBarButtonItem BaseSubmitButton => _submitButton;
public override Action Success => () => LoadingController.DismissLockAndContinue(); public override Action Success => () =>
public override Action Cancel => () => LoadingController.CompleteRequest(); {
LoadingController?.Navigate(Bit.Core.Enums.NavigationTarget.Home);
LoadingController = null;
};
public override Action Cancel => () =>
{
LoadingController?.CompleteRequest();
LoadingController = null;
};
public override void ViewDidLoad() public override UITableView TableView => _mainTableView;
public override async void ViewDidLoad()
{ {
base.ViewDidLoad(); base.ViewDidLoad();
_cancelButton.TintColor = ThemeHelpers.NavBarTextColor; _cancelButton.TintColor = ThemeHelpers.NavBarTextColor;
_submitButton.TintColor = ThemeHelpers.NavBarTextColor; _submitButton.TintColor = ThemeHelpers.NavBarTextColor;
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
_accountSwitchingButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync();
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(_overlayView);
}
protected override void UpdateNavigationBarTheme()
{
UpdateNavigationBarTheme(_navBar);
}
partial void AccountSwitchingButton_Activated(UIBarButtonItem sender)
{
_accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView);
} }
partial void SubmitButton_Activated(UIBarButtonItem sender) partial void SubmitButton_Activated(UIBarButtonItem sender)
{ {
var task = CheckPasswordAsync(); CheckPasswordAsync().FireAndForget();
} }
partial void CancelButton_Activated(UIBarButtonItem sender) partial void CancelButton_Activated(UIBarButtonItem sender)
{ {
Cancel(); Cancel();
} }
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (TableView != null)
{
TableView.Source?.Dispose();
}
if (_accountSwitchingButton?.Image != null)
{
var img = _accountSwitchingButton.Image;
_accountSwitchingButton.Image = null;
img.Dispose();
}
if (_accountSwitchingOverlayView != null && _overlayView?.Subviews != null)
{
foreach (var subView in _overlayView.Subviews)
{
subView.RemoveFromSuperview();
subView.Dispose();
}
_accountSwitchingOverlayView = null;
_overlayView.RemoveFromSuperview();
}
_accountSwitchingOverlayHelper = null;
}
base.Dispose(disposing);
}
} }
} }

View File

@ -12,18 +12,30 @@ namespace Bit.iOS.ShareExtension
[Register ("LockPasswordViewController")] [Register ("LockPasswordViewController")]
partial class LockPasswordViewController partial class LockPasswordViewController
{ {
[Outlet]
UIKit.UIBarButtonItem _accountSwitchingButton { get; set; }
[Outlet] [Outlet]
UIKit.UIBarButtonItem _cancelButton { get; set; } UIKit.UIBarButtonItem _cancelButton { get; set; }
[Outlet] [Outlet]
UIKit.UITableView _mainTableView { get; set; } UIKit.UITableView _mainTableView { get; set; }
[Outlet]
UIKit.UINavigationBar _navBar { get; set; }
[Outlet] [Outlet]
UIKit.UINavigationItem _navItem { get; set; } UIKit.UINavigationItem _navItem { get; set; }
[Outlet]
UIKit.UIView _overlayView { get; set; }
[Outlet] [Outlet]
UIKit.UIBarButtonItem _submitButton { get; set; } UIKit.UIBarButtonItem _submitButton { get; set; }
[Action ("AccountSwitchingButton_Activated:")]
partial void AccountSwitchingButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("CancelButton_Activated:")] [Action ("CancelButton_Activated:")]
partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); partial void CancelButton_Activated (UIKit.UIBarButtonItem sender);
@ -32,6 +44,11 @@ namespace Bit.iOS.ShareExtension
void ReleaseDesignerOutlets () void ReleaseDesignerOutlets ()
{ {
if (_accountSwitchingButton != null) {
_accountSwitchingButton.Dispose ();
_accountSwitchingButton = null;
}
if (_cancelButton != null) { if (_cancelButton != null) {
_cancelButton.Dispose (); _cancelButton.Dispose ();
_cancelButton = null; _cancelButton = null;
@ -47,10 +64,20 @@ namespace Bit.iOS.ShareExtension
_navItem = null; _navItem = null;
} }
if (_overlayView != null) {
_overlayView.Dispose ();
_overlayView = null;
}
if (_submitButton != null) { if (_submitButton != null) {
_submitButton.Dispose (); _submitButton.Dispose ();
_submitButton = null; _submitButton = null;
} }
if (_navBar != null) {
_navBar.Dispose ();
_navBar = null;
}
} }
} }
} }

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="2vH-Do-uhk"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="2vH-Do-uhk">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -11,10 +13,6 @@
<scene sceneID="kFr-IN-5GS"> <scene sceneID="kFr-IN-5GS">
<objects> <objects>
<viewController id="bHU-LX-EpF" customClass="LoadingViewController" sceneMemberID="viewController"> <viewController id="bHU-LX-EpF" customClass="LoadingViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="8LE-gl-yDT"/>
<viewControllerLayoutGuide type="bottom" id="MuK-nA-9iu"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="z2O-Vp-jY9"> <view key="view" contentMode="scaleToFill" id="z2O-Vp-jY9">
<rect key="frame" x="0.0" y="0.0" width="414" height="808"/> <rect key="frame" x="0.0" y="0.0" width="414" height="808"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
@ -23,25 +21,25 @@
<rect key="frame" x="66" y="352" width="282" height="44"/> <rect key="frame" x="66" y="352" width="282" height="44"/>
</imageView> </imageView>
</subviews> </subviews>
<viewLayoutGuide key="safeArea" id="jNx-Vd-K6U"/>
<constraints> <constraints>
<constraint firstItem="Zdy-yw-n0p" firstAttribute="centerX" secondItem="z2O-Vp-jY9" secondAttribute="centerX" id="6DT-HB-vS5"/> <constraint firstItem="Zdy-yw-n0p" firstAttribute="centerX" secondItem="jNx-Vd-K6U" secondAttribute="centerX" id="6DT-HB-vS5"/>
<constraint firstItem="Zdy-yw-n0p" firstAttribute="centerY" secondItem="z2O-Vp-jY9" secondAttribute="centerY" constant="-30" id="o9N-Tv-Iwq"/> <constraint firstItem="Zdy-yw-n0p" firstAttribute="centerY" secondItem="z2O-Vp-jY9" secondAttribute="centerY" constant="-30" id="o9N-Tv-Iwq"/>
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" id="74l-Va-Vqa"/> <navigationItem key="navigationItem" id="74l-Va-Vqa"/>
<connections> <connections>
<outlet property="Logo" destination="Zdy-yw-n0p" id="1Qk-EK-0BO"/> <outlet property="Logo" destination="Zdy-yw-n0p" id="1Qk-EK-0BO"/>
<segue destination="rh6-Mf-4Ja" kind="presentation" identifier="lockPasswordSegue" id="ZUl-jv-5se"/>
</connections> </connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yJx-cc-wzs" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="yJx-cc-wzs" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-374" y="560"/> <point key="canvasLocation" x="-374" y="560"/>
</scene> </scene>
<!--Navigation Controller--> <!--Extension Navigation Controller-->
<scene sceneID="Wgx-vz-XqL"> <scene sceneID="Wgx-vz-XqL">
<objects> <objects>
<navigationController definesPresentationContext="YES" id="2vH-Do-uhk" sceneMemberID="viewController"> <navigationController definesPresentationContext="YES" id="2vH-Do-uhk" customClass="ExtensionNavigationController" sceneMemberID="viewController">
<navigationBar key="navigationBar" hidden="YES" contentMode="scaleToFill" translucent="NO" id="JoO-jQ-16M"> <navigationBar key="navigationBar" hidden="YES" contentMode="scaleToFill" translucent="NO" id="JoO-jQ-16M">
<rect key="frame" x="0.0" y="44" width="414" height="44"/> <rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
@ -54,60 +52,89 @@
</objects> </objects>
<point key="canvasLocation" x="-1097" y="564"/> <point key="canvasLocation" x="-1097" y="564"/>
</scene> </scene>
<!--Navigation Controller--> <!--Lock Password View Controller-->
<scene sceneID="Tzp-2o-9k7"> <scene sceneID="vQB-cT-8IC">
<objects> <objects>
<navigationController definesPresentationContext="YES" id="rh6-Mf-4Ja" sceneMemberID="viewController"> <viewController storyboardIdentifier="lockVC" id="Vi7-LV-nWW" customClass="LockPasswordViewController" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" translucent="NO" id="UDq-kw-Ue7"> <view key="view" contentMode="scaleToFill" id="Vfd-7B-19G">
<rect key="frame" x="0.0" y="0.0" width="414" height="56"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</navigationBar>
<connections>
<segue destination="85y-W9-d8q" kind="relationship" relationship="rootViewController" id="TeA-GE-A22"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="BVV-5B-aim" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-375" y="1262"/>
</scene>
<!--Verify Master Password-->
<scene sceneID="OEb-ak-BVc">
<objects>
<tableViewController id="85y-W9-d8q" customClass="LockPasswordViewController" sceneMemberID="viewController">
<tableView key="view" opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="9on-wf-zdb">
<rect key="frame" x="0.0" y="0.0" width="414" height="786"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <subviews>
<connections> <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="M1A-84-x5l">
<outlet property="dataSource" destination="85y-W9-d8q" id="3il-RO-S3K"/> <rect key="frame" x="0.0" y="88" width="414" height="774"/>
<outlet property="delegate" destination="85y-W9-d8q" id="bLb-h4-pr3"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</connections>
</tableView> </tableView>
<navigationItem key="navigationItem" title="Verify Master Password" id="qL3-iV-6Ld"> <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ijE-Pa-OBq" userLabel="OverlayView">
<barButtonItem key="leftBarButtonItem" title="Cancel" id="d8j-HZ-erD"> <rect key="frame" x="0.0" y="88" width="414" height="774"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fav-Fz-6ZK">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<items>
<navigationItem title="Verify Master Password" id="aka-In-IYk">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="LrG-Qx-w4Q">
<connections> <connections>
<action selector="CancelButton_Activated:" destination="85y-W9-d8q" id="p54-B0-Vyf"/> <action selector="CancelButton_Activated:" destination="Vi7-LV-nWW" id="qyZ-i9-Dwz"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Submit" id="8a7-Vz-SJA"> <barButtonItem title="Item" image="person.2" catalog="system" style="plain" id="nlD-Xn-HtM" userLabel="Account Switching Button">
<color key="tintColor" systemColor="systemBackgroundColor"/>
<connections> <connections>
<action selector="SubmitButton_Activated:" destination="85y-W9-d8q" id="P8A-7O-lpY"/> <action selector="AccountSwitchingButton_Activated:" destination="Vi7-LV-nWW" id="G3U-rv-UOl"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" title="Submit" id="oQD-QK-YPB">
<connections>
<action selector="SubmitButton_Activated:" destination="Vi7-LV-nWW" id="DgO-TS-MPf"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
</items>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="barPosition">
<integer key="value" value="3"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</navigationBar>
</subviews>
<viewLayoutGuide key="safeArea" id="SSW-s3-JwL"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="M1A-84-x5l" firstAttribute="leading" secondItem="SSW-s3-JwL" secondAttribute="leading" id="3Es-aL-5Og"/>
<constraint firstItem="ijE-Pa-OBq" firstAttribute="leading" secondItem="SSW-s3-JwL" secondAttribute="leading" id="6Lj-CR-OFz"/>
<constraint firstItem="fav-Fz-6ZK" firstAttribute="leading" secondItem="SSW-s3-JwL" secondAttribute="leading" id="BEJ-gh-NAq"/>
<constraint firstItem="fav-Fz-6ZK" firstAttribute="top" secondItem="SSW-s3-JwL" secondAttribute="top" id="CLE-2p-LI3"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="trailing" secondItem="M1A-84-x5l" secondAttribute="trailing" id="GaL-B0-2Lg"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="bottom" secondItem="M1A-84-x5l" secondAttribute="bottom" id="LG1-vj-VhW"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="trailing" secondItem="ijE-Pa-OBq" secondAttribute="trailing" id="Q3J-Wa-mnY"/>
<constraint firstItem="ijE-Pa-OBq" firstAttribute="top" secondItem="fav-Fz-6ZK" secondAttribute="bottom" id="h8T-rn-ZPU"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="trailing" secondItem="fav-Fz-6ZK" secondAttribute="trailing" id="tux-AN-Z92"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="bottom" secondItem="ijE-Pa-OBq" secondAttribute="bottom" id="zLh-RX-eSc"/>
<constraint firstItem="M1A-84-x5l" firstAttribute="top" secondItem="fav-Fz-6ZK" secondAttribute="bottom" id="zgM-he-DYl"/>
</constraints>
</view>
<connections> <connections>
<outlet property="_cancelButton" destination="d8j-HZ-erD" id="wlI-el-Snh"/> <outlet property="_accountSwitchingButton" destination="nlD-Xn-HtM" id="SSG-zv-bAc"/>
<outlet property="_mainTableView" destination="9on-wf-zdb" id="ltj-yY-5ue"/> <outlet property="_cancelButton" destination="LrG-Qx-w4Q" id="aag-ZZ-Ifs"/>
<outlet property="_navItem" destination="qL3-iV-6Ld" id="Grb-Ta-NCF"/> <outlet property="_mainTableView" destination="M1A-84-x5l" id="pA4-ao-Fhu"/>
<outlet property="_submitButton" destination="8a7-Vz-SJA" id="LS8-6Y-Wkp"/> <outlet property="_navBar" destination="fav-Fz-6ZK" id="Q9p-Dw-ipx"/>
<outlet property="_navItem" destination="aka-In-IYk" id="www-Lt-x1g"/>
<outlet property="_overlayView" destination="ijE-Pa-OBq" id="n9e-Lg-4WO"/>
<outlet property="_submitButton" destination="oQD-QK-YPB" id="SEp-KK-YeP"/>
</connections> </connections>
</tableViewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="5by-Sa-d9m" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="Czu-9n-yKC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="335" y="1260"/> <point key="canvasLocation" x="403" y="560"/>
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="logo.png" width="282" height="44"/> <image name="logo.png" width="282" height="44"/>
<image name="person.2" catalog="system" width="128" height="81"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources> </resources>
</document> </document>

View File

@ -26,7 +26,6 @@
<MtouchLink>None</MtouchLink> <MtouchLink>None</MtouchLink>
<MtouchArch>x86_64</MtouchArch> <MtouchArch>x86_64</MtouchArch>
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler> <MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
<DeviceSpecificBuild>false</DeviceSpecificBuild>
<MtouchVerbosity></MtouchVerbosity> <MtouchVerbosity></MtouchVerbosity>
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements> <CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<AssemblyName>BitwardeniOSShareExtension</AssemblyName> <AssemblyName>BitwardeniOSShareExtension</AssemblyName>
@ -193,6 +192,10 @@
<Compile Include="LockPasswordViewController.designer.cs"> <Compile Include="LockPasswordViewController.designer.cs">
<DependentUpon>LockPasswordViewController.cs</DependentUpon> <DependentUpon>LockPasswordViewController.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="ExtensionNavigationController.cs" />
<Compile Include="ExtensionNavigationController.designer.cs">
<DependentUpon>ExtensionNavigationController.cs</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\App\App.csproj"> <ProjectReference Include="..\App\App.csproj">