diff --git a/.github/resources/export-options-app-store.plist b/.github/resources/export-options-app-store.plist
index df3ed635b..dc991f2a8 100644
--- a/.github/resources/export-options-app-store.plist
+++ b/.github/resources/export-options-app-store.plist
@@ -14,6 +14,10 @@
Dist: Extension 2021
com.8bit.bitwarden.share-extension
Dist: Share Extension 2021
+ com.8bit.bitwarden.watchkitapp
+ Dist: Bitwarden Watch App
+ com.8bit.bitwarden.watchkitapp.watchkitextension
+ Dist: Bitwarden Watch App Extension
diff --git a/.github/secrets/dist_watch_app.mobileprovision.gpg b/.github/secrets/dist_watch_app.mobileprovision.gpg
new file mode 100644
index 000000000..8981acabe
Binary files /dev/null and b/.github/secrets/dist_watch_app.mobileprovision.gpg differ
diff --git a/.github/secrets/dist_watch_app_extension.mobileprovision.gpg b/.github/secrets/dist_watch_app_extension.mobileprovision.gpg
new file mode 100644
index 000000000..57b1bf2dd
Binary files /dev/null and b/.github/secrets/dist_watch_app_extension.mobileprovision.gpg differ
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f4fe0678f..2b20c3ffc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -58,6 +58,7 @@ jobs:
android:
name: Android
runs-on: windows-2022
+ if: github.repository == 'putting here a false condition so that this doesnt run while testing iOS CI'
needs: setup
strategy:
fail-fast: false
@@ -258,6 +259,7 @@ jobs:
f-droid:
name: F-Droid Build
runs-on: windows-2022
+ if: github.repository == 'putting here a false condition so that this doesnt run while testing iOS CI'
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
@@ -497,6 +499,12 @@ jobs:
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output $HOME/secrets/dist_share_extension.mobileprovision \
./.github/secrets/dist_share_extension.mobileprovision.gpg
+ gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
+ --output $HOME/secrets/dist_watch_app.mobileprovision \
+ ./.github/secrets/dist_watch_app.mobileprovision.gpg
+ gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
+ --output $HOME/secrets/dist_watch_app_extension.mobileprovision \
+ ./.github/secrets/dist_watch_app_extension.mobileprovision.gpg
shell: bash
- name: Increment version
@@ -511,6 +519,9 @@ jobs:
perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist
perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist
perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist
+ cd src/watchOS/bitwarden
+ agvtool new-version -all $BUILD_NUMBER
+ cd ../../..
shell: bash
- name: Update Entitlements
@@ -545,6 +556,8 @@ jobs:
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision
+ WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_watch_app.mobileprovision
+ WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_watch_app_extension.mobileprovision
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
mkdir -p "$PROFILES_DIR_PATH"
@@ -560,6 +573,28 @@ jobs:
SHARE_EXTENSION_UUID=$(grep UUID -A1 -a $SHARE_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
cp $SHARE_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$SHARE_EXTENSION_UUID.mobileprovision"
+
+ WATCH_APP_UUID=$(grep UUID -A1 -a $WATCH_APP_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
+ cp $WATCH_APP_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_UUID.mobileprovision"
+
+ WATCH_APP_EXTENSION_UUID=$(grep UUID -A1 -a $WATCH_APP_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
+ cp $WATCH_APP_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_EXTENSION_UUID.mobileprovision"
+ shell: bash
+
+ - name: Bulid WatchApp
+ run: |
+ echo "########################################"
+ echo "##### Build WatchApp with Release Configuration"
+ echo "########################################"
+
+ xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden
+
+ echo "########################################"
+ echo "##### Done"
+ echo "########################################"
+ cd src/watchOS
+ ls -R
+ cd ../..
shell: bash
- name: Restore packages
diff --git a/.gitignore b/.gitignore
index 67fa6064c..107fad1df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -210,3 +210,128 @@ project.lock.json
.DS_Store
src/App/Css
tools
+
+# Created by https://www.toptal.com/developers/gitignore/api/swift,objective-c
+# Edit at https://www.toptal.com/developers/gitignore?templates=swift,objective-c
+
+### Objective-C ###
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+# CocoaPods
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+# Pods/
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# fastlane
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+
+### Objective-C Patch ###
+
+### Swift ###
+# Xcode
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+
+
+
+
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+# Pods/
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+
+# Code Injection
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+
+# End of https://www.toptal.com/developers/gitignore/api/swift,objective-c
diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index 73a19d10e..8d01e1091 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -159,6 +159,7 @@
+
diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs
index 8bca7cafa..56aafe4e5 100644
--- a/src/Android/MainApplication.cs
+++ b/src/Android/MainApplication.cs
@@ -45,9 +45,16 @@ namespace Bit.Droid
if (ServiceContainer.RegisteredServices.Count == 0)
{
RegisterLocalServices();
+
var deviceActionService = ServiceContainer.Resolve("deviceActionService");
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Core.Constants.ClearCiphersCacheKey,
Core.Constants.AndroidAllClearCipherCacheKeys);
+
+ ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve()));
+
InitializeAppSetup();
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
@@ -73,8 +80,9 @@ namespace Bit.Droid
ServiceContainer.Resolve("platformUtilsService"),
ServiceContainer.Resolve("authService"),
ServiceContainer.Resolve("logger"),
- ServiceContainer.Resolve("messagingService"));
- ServiceContainer.Register("accountsManager", accountsManager);
+ ServiceContainer.Resolve("messagingService"),
+ ServiceContainer.Resolve());
+ ServiceContainer.Register("accountsManager", accountsManager);
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs
index f189c77ce..82d6f9fd8 100644
--- a/src/Android/Services/DeviceActionService.cs
+++ b/src/Android/Services/DeviceActionService.cs
@@ -18,6 +18,7 @@ using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
+using static Bit.App.Pages.SettingsPageViewModel;
namespace Bit.Droid.Services
{
diff --git a/src/Android/Services/WatchDeviceService.cs b/src/Android/Services/WatchDeviceService.cs
new file mode 100644
index 000000000..0cad001b3
--- /dev/null
+++ b/src/Android/Services/WatchDeviceService.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Threading.Tasks;
+using Bit.App.Services;
+using Bit.Core.Abstractions;
+using Bit.Core.Models;
+
+namespace Bit.Droid.Services
+{
+ public class WatchDeviceService : BaseWatchDeviceService
+ {
+ public WatchDeviceService(ICipherService cipherService,
+ IEnvironmentService environmentService,
+ IStateService stateService,
+ IVaultTimeoutService vaultTimeoutService)
+ : base(cipherService, environmentService, stateService, vaultTimeoutService)
+ {
+ }
+
+ protected override bool IsSupported => false;
+
+ public override bool IsConnected => false;
+
+ protected override bool CanSendData => false;
+
+ protected override Task SendDataToWatchAsync(WatchDTO watchDto) => throw new NotImplementedException();
+
+ protected override void ConnectToWatch() => throw new NotImplementedException();
+ }
+}
diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs
index 70384100e..e539b156f 100644
--- a/src/App/Abstractions/IDeviceActionService.cs
+++ b/src/App/Abstractions/IDeviceActionService.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
+using Bit.Core.Models;
namespace Bit.App.Abstractions
{
diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs
index 5419916e9..5d2fccfd3 100644
--- a/src/App/Pages/Accounts/LockPageViewModel.cs
+++ b/src/App/Pages/Accounts/LockPageViewModel.cs
@@ -28,6 +28,7 @@ namespace Bit.App.Pages
private readonly IBiometricService _biometricService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly ILogger _logger;
+ private readonly IWatchDeviceService _watchDeviceService;
private readonly WeakEventManager _secretEntryFocusWeakEventManager = new WeakEventManager();
private string _email;
@@ -56,6 +57,7 @@ namespace Bit.App.Pages
_biometricService = ServiceContainer.Resolve("biometricService");
_keyConnectorService = ServiceContainer.Resolve("keyConnectorService");
_logger = ServiceContainer.Resolve("logger");
+ _watchDeviceService = ServiceContainer.Resolve();
PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
@@ -387,6 +389,7 @@ namespace Bit.App.Pages
private async Task DoContinueAsync()
{
await _stateService.SetBiometricLockedAsync(false);
+ _watchDeviceService.SyncDataToWatchAsync().FireAndForget();
_messagingService.Send("unlocked");
UnlockedAction?.Invoke();
}
diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
index f586e6e79..bf00f5a3e 100644
--- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
+++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
@@ -7,7 +7,10 @@ using Bit.App.Pages.Accounts;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
+using Bit.Core.Models;
using Bit.Core.Models.Domain;
+using Bit.Core.Models.View;
+using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
@@ -32,6 +35,7 @@ namespace Bit.App.Pages
private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService;
private readonly IPushNotificationService _pushNotificationService;
+ private readonly IWatchDeviceService _watchDeviceService;
private const int CustomVaultTimeoutValue = -100;
private bool _supportsBiometric;
@@ -44,6 +48,7 @@ namespace Bit.App.Pages
private bool _showChangeMasterPassword;
private bool _reportLoggingEnabled;
private bool _approvePasswordlessLoginRequests;
+ private bool _shouldConnectToWatch;
private List> _vaultTimeouts =
new List>
@@ -87,6 +92,7 @@ namespace Bit.App.Pages
_clipboardService = ServiceContainer.Resolve("clipboardService");
_loggerService = ServiceContainer.Resolve("logger");
_pushNotificationService = ServiceContainer.Resolve();
+ _watchDeviceService = ServiceContainer.Resolve();
GroupedItems = new ObservableRangeCollection();
PageTitle = AppResources.Settings;
@@ -138,6 +144,9 @@ namespace Bit.App.Pages
!await _keyConnectorService.GetUsesKeyConnector();
_reportLoggingEnabled = await _loggerService.IsEnabled();
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
+
+ _shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
+
BuildList();
}
@@ -601,19 +610,26 @@ namespace Bit.App.Pages
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
});
}
- var accountItems = new List
+ var accountItems = new List();
+ if (Device.RuntimePlatform == Device.iOS)
{
- new SettingsPageListItem
+ accountItems.Add(new SettingsPageListItem
{
- Name = AppResources.FingerprintPhrase,
- ExecuteAsync = () => FingerprintAsync()
- },
- new SettingsPageListItem
- {
- Name = AppResources.LogOut,
- ExecuteAsync = () => LogOutAsync()
- }
- };
+ Name = AppResources.ConnectToWatch,
+ SubLabel = _shouldConnectToWatch ? AppResources.On : AppResources.Off,
+ ExecuteAsync = () => ToggleWatchConnectionAsync()
+ });
+ }
+ accountItems.Add(new SettingsPageListItem
+ {
+ Name = AppResources.FingerprintPhrase,
+ ExecuteAsync = () => FingerprintAsync()
+ });
+ accountItems.Add(new SettingsPageListItem
+ {
+ Name = AppResources.LogOut,
+ ExecuteAsync = () => LogOutAsync()
+ });
if (_showChangeMasterPassword)
{
accountItems.Insert(0, new SettingsPageListItem
@@ -791,5 +807,13 @@ namespace Bit.App.Pages
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
+
+ private async Task ToggleWatchConnectionAsync()
+ {
+ _shouldConnectToWatch = !_shouldConnectToWatch;
+
+ await _watchDeviceService.SetShouldConnectToWatchAsync(_shouldConnectToWatch);
+ BuildList();
+ }
}
}
diff --git a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
index 4f8ea42a1..67ae693e0 100644
--- a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
+++ b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
@@ -29,6 +29,7 @@ namespace Bit.App.Pages
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly IClipboardService _clipboardService;
private readonly IAutofillHandler _autofillHandler;
+ private readonly IWatchDeviceService _watchDeviceService;
private bool _showNotesSeparator;
private bool _showPassword;
@@ -80,6 +81,7 @@ namespace Bit.App.Pages
_customFieldItemFactory = ServiceContainer.Resolve("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve("clipboardService");
_autofillHandler = ServiceContainer.Resolve();
+ _watchDeviceService = ServiceContainer.Resolve();
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
@@ -507,6 +509,8 @@ namespace Bit.App.Pages
EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
_messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id);
+ _watchDeviceService.SyncDataToWatchAsync().FireAndForget();
+
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
{
// Close and go back to app
diff --git a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs
index dcd994335..f0005ecf2 100644
--- a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs
+++ b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs
@@ -31,6 +31,7 @@ namespace Bit.App.Pages
private readonly ILocalizeService _localizeService;
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly IClipboardService _clipboardService;
+ private readonly IWatchDeviceService _watchDeviceService;
private List _fields;
private bool _canAccessPremium;
@@ -62,6 +63,7 @@ namespace Bit.App.Pages
_localizeService = ServiceContainer.Resolve("localizeService");
_customFieldItemFactory = ServiceContainer.Resolve("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve("clipboardService");
+ _watchDeviceService = ServiceContainer.Resolve();
CopyCommand = new AsyncCommand((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
CopyUriCommand = new AsyncCommand(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
@@ -371,6 +373,9 @@ namespace Bit.App.Pages
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
}
await _deviceActionService.HideLoadingAsync();
+
+ _watchDeviceService.SyncDataToWatchAsync().FireAndForget();
+
_platformUtilsService.ShowToast("success", null,
Cipher.IsDeleted ? AppResources.ItemDeleted : AppResources.ItemSoftDeleted);
_messagingService.Send(Cipher.IsDeleted ? "deletedCipher" : "softDeletedCipher", Cipher);
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index ca0e23b08..7285cbeaa 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -1498,6 +1498,15 @@ namespace Bit.App.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Connect to Watch.
+ ///
+ public static string ConnectToWatch {
+ get {
+ return ResourceManager.GetString("ConnectToWatch", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Continue.
///
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 82ed75f6b..ee49befd5 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -2453,6 +2453,9 @@ select Add TOTP to store the key safely
Random
+
+ Connect to Watch
+
Accessibility Service Disclosure
diff --git a/src/App/Services/BaseWatchDeviceService.cs b/src/App/Services/BaseWatchDeviceService.cs
new file mode 100644
index 000000000..f2ffc0090
--- /dev/null
+++ b/src/App/Services/BaseWatchDeviceService.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Bit.Core.Abstractions;
+using Bit.Core.Enums;
+using Bit.Core.Models;
+using Bit.Core.Models.View;
+using Xamarin.Forms;
+
+namespace Bit.App.Services
+{
+ public abstract class BaseWatchDeviceService : IWatchDeviceService
+ {
+ private readonly ICipherService _cipherService;
+ private readonly IEnvironmentService _environmentService;
+ private readonly IStateService _stateService;
+ private readonly IVaultTimeoutService _vaultTimeoutService;
+
+ protected BaseWatchDeviceService(ICipherService cipherService,
+ IEnvironmentService environmentService,
+ IStateService stateService,
+ IVaultTimeoutService vaultTimeoutService)
+ {
+ _cipherService = cipherService;
+ _environmentService = environmentService;
+ _stateService = stateService;
+ _vaultTimeoutService = vaultTimeoutService;
+ }
+
+ public abstract bool IsConnected { get; }
+
+ protected abstract bool CanSendData { get; }
+ protected abstract bool IsSupported { get; }
+
+ public async Task SyncDataToWatchAsync()
+ {
+ if (!IsSupported)
+ {
+ return;
+ }
+
+ var shouldConnect = await _stateService.GetShouldConnectToWatchAsync();
+ if (shouldConnect && !IsConnected)
+ {
+ ConnectToWatch();
+ }
+
+ if (!CanSendData)
+ {
+ return;
+ }
+
+ var userData = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null ? null : new WatchDTO.UserDataDto
+ {
+ Id = a.Profile.UserId,
+ Name = a.Profile.Name,
+ Email = a.Profile.Email
+ });
+ var state = await GetStateAsync(userData?.Id, shouldConnect);
+ if (state != WatchState.Valid)
+ {
+ await SendDataToWatchAsync(new WatchDTO(state));
+ return;
+ }
+
+ var ciphersWithTotp = await _cipherService.GetAllDecryptedAsync(c => c.DeletedDate == null && c.Login?.Totp != null);
+
+ if (!ciphersWithTotp.Any())
+ {
+ await SendDataToWatchAsync(new WatchDTO(WatchState.Need2FAItem));
+ return;
+ }
+
+ var watchDto = new WatchDTO(state)
+ {
+ Ciphers = ciphersWithTotp.Select(c => new SimpleCipherView(c)).ToList(),
+ UserData = userData,
+ EnvironmentData = new WatchDTO.EnvironmentUrlDataDto
+ {
+ Base = _environmentService.BaseUrl,
+ Icons = _environmentService.IconsUrl
+ }
+ //SettingsData = new WatchDTO.SettingsDataDto
+ //{
+ // VaultTimeoutInMinutes = await _vaultTimeoutService.GetVaultTimeout(userData?.Id),
+ // VaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userData?.Id) ?? VaultTimeoutAction.Lock
+ //}
+ };
+ await SendDataToWatchAsync(watchDto);
+ }
+
+ private async Task GetStateAsync(string userId, bool shouldConnectToWatch)
+ {
+ if (!shouldConnectToWatch)
+ {
+ return WatchState.NeedSetup;
+ }
+
+ if (!await _stateService.IsAuthenticatedAsync() || userId is null)
+ {
+ return WatchState.NeedLogin;
+ }
+
+ //if (await _vaultTimeoutService.IsLockedAsync() ||
+ // await _vaultTimeoutService.ShouldLockAsync())
+ //{
+ // return WatchState.NeedUnlock;
+ //}
+
+ if (!await _stateService.CanAccessPremiumAsync(userId))
+ {
+ return WatchState.NeedPremium;
+ }
+
+ return WatchState.Valid;
+ }
+
+ public async Task SetShouldConnectToWatchAsync(bool shouldConnectToWatch)
+ {
+ await _stateService.SetShouldConnectToWatchAsync(shouldConnectToWatch);
+ await SyncDataToWatchAsync();
+ }
+
+ protected abstract Task SendDataToWatchAsync(WatchDTO watchDto);
+
+ protected abstract void ConnectToWatch();
+ }
+}
diff --git a/src/App/Utilities/AccountManagement/AccountsManager.cs b/src/App/Utilities/AccountManagement/AccountsManager.cs
index b804823f2..c87671073 100644
--- a/src/App/Utilities/AccountManagement/AccountsManager.cs
+++ b/src/App/Utilities/AccountManagement/AccountsManager.cs
@@ -22,6 +22,8 @@ namespace Bit.App.Utilities.AccountManagement
private readonly IAuthService _authService;
private readonly ILogger _logger;
private readonly IMessagingService _messagingService;
+ private readonly IWatchDeviceService _watchDeviceService;
+
Func _getOptionsFunc;
private IAccountsManagerHost _accountsManagerHost;
@@ -32,7 +34,8 @@ namespace Bit.App.Utilities.AccountManagement
IPlatformUtilsService platformUtilsService,
IAuthService authService,
ILogger logger,
- IMessagingService messagingService)
+ IMessagingService messagingService,
+ IWatchDeviceService watchDeviceService)
{
_broadcasterService = broadcasterService;
_vaultTimeoutService = vaultTimeoutService;
@@ -42,6 +45,7 @@ namespace Bit.App.Utilities.AccountManagement
_authService = authService;
_logger = logger;
_messagingService = messagingService;
+ _watchDeviceService = watchDeviceService;
}
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
@@ -145,6 +149,7 @@ namespace Bit.App.Utilities.AccountManagement
break;
case AccountsManagerMessageCommands.SWITCHED_ACCOUNT:
await SwitchedAccountAsync();
+
break;
}
}
@@ -217,6 +222,7 @@ namespace Bit.App.Utilities.AccountManagement
}
await Task.Delay(50);
await _accountsManagerHost.UpdateThemeAsync();
+ _watchDeviceService.SyncDataToWatchAsync().FireAndForget();
_messagingService.Send(AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED);
});
}
diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs
index 60e4b722c..e34aaa068 100644
--- a/src/Core/Abstractions/ICipherService.cs
+++ b/src/Core/Abstractions/ICipherService.cs
@@ -19,7 +19,7 @@ namespace Bit.Core.Abstractions
Task DeleteWithServerAsync(string id);
Task EncryptAsync(CipherView model, SymmetricCryptoKey key = null, Cipher originalCipher = null);
Task> GetAllAsync();
- Task> GetAllDecryptedAsync();
+ Task> GetAllDecryptedAsync(Func filter = null);
Task, List, List>> GetAllDecryptedByUrlAsync(string url,
List includeOtherTypes = null);
Task> GetAllDecryptedForGroupingAsync(string groupingId, bool folder = true);
diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs
index 63a020403..9525ed481 100644
--- a/src/Core/Abstractions/IStateService.cs
+++ b/src/Core/Abstractions/IStateService.cs
@@ -14,6 +14,7 @@ namespace Bit.Core.Abstractions
List AccountViews { get; }
Task GetActiveUserIdAsync();
Task GetActiveUserEmailAsync();
+ Task GetActiveUserCustomDataAsync(Func dataMapper);
Task IsActiveAccountAsync(string userId = null);
Task SetActiveUserAsync(string userId);
Task CheckExtensionActiveUserAndSwitchIfNeededAsync();
@@ -159,5 +160,7 @@ namespace Bit.Core.Abstractions
Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value);
Task GetUsernameGenerationOptionsAsync(string userId = null);
Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
+ Task GetShouldConnectToWatchAsync(string userId = null);
+ Task SetShouldConnectToWatchAsync(bool shouldConnect, string userId = null);
}
}
diff --git a/src/Core/Abstractions/IWatchDeviceService.cs b/src/Core/Abstractions/IWatchDeviceService.cs
new file mode 100644
index 000000000..0edfb2afe
--- /dev/null
+++ b/src/Core/Abstractions/IWatchDeviceService.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace Bit.Core.Abstractions
+{
+ public interface IWatchDeviceService
+ {
+ bool IsConnected { get; }
+
+ Task SetShouldConnectToWatchAsync(bool shouldConnectToWatch);
+ Task SyncDataToWatchAsync();
+ }
+}
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index c31b0d175..06d3a19a7 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -96,5 +96,6 @@
public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}";
public static string ApprovePasswordlessLoginsKey(string userId) => $"approvePasswordlessLogins_{userId}";
public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}";
+ public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}";
}
}
diff --git a/src/Core/Enums/WatchState.cs b/src/Core/Enums/WatchState.cs
new file mode 100644
index 000000000..8452e8d68
--- /dev/null
+++ b/src/Core/Enums/WatchState.cs
@@ -0,0 +1,13 @@
+namespace Bit.Core.Enums
+{
+ public enum WatchState : byte
+ {
+ Valid = 0,
+ NeedLogin,
+ NeedPremium,
+ NeedSetup,
+ Need2FAItem,
+ Syncing
+ //NeedUnlock
+ }
+}
diff --git a/src/Core/Models/View/SimpleCipherView.cs b/src/Core/Models/View/SimpleCipherView.cs
new file mode 100644
index 000000000..df5262710
--- /dev/null
+++ b/src/Core/Models/View/SimpleCipherView.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Linq;
+using Bit.Core.Enums;
+
+namespace Bit.Core.Models.View
+{
+ public class SimpleCipherView
+ {
+ public SimpleCipherView(CipherView c)
+ {
+ Id = c.Id;
+ Name = c.Name;
+ Type = c.Type;
+ if (c.Login != null)
+ {
+ Login = new SimpleLoginView
+ {
+ Username = c.Login.Username,
+ Totp = c.Login.Totp,
+ Uris = c.Login.Uris?.Select(u => new SimpleLoginUriView(u.Uri)).ToList()
+ };
+ }
+ }
+
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public CipherType Type { get; set; }
+ public SimpleLoginView Login { get; set; }
+ }
+
+ public class SimpleLoginView
+ {
+ public string Username { get; set; }
+ public string Totp { get; set; }
+ public List Uris { get; set; }
+ }
+
+ public class SimpleLoginUriView
+ {
+ public SimpleLoginUriView(string uri)
+ {
+ Uri = uri;
+ }
+
+ public string Uri { get; set; }
+ }
+}
+
diff --git a/src/Core/Models/View/WatchDTO.cs b/src/Core/Models/View/WatchDTO.cs
new file mode 100644
index 000000000..33bcae973
--- /dev/null
+++ b/src/Core/Models/View/WatchDTO.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using Bit.Core.Enums;
+using Bit.Core.Models.View;
+
+namespace Bit.Core.Models
+{
+ public class WatchDTO
+ {
+ public WatchDTO(WatchState state)
+ {
+ State = state;
+ }
+
+ public WatchState State { get; private set; }
+
+ public List Ciphers { get; set; }
+
+ public UserDataDto UserData { get; set; }
+
+ public EnvironmentUrlDataDto EnvironmentData { get; set; }
+
+ //public SettingsDataDto SettingsData { get; set; }
+
+ public class UserDataDto
+ {
+ public string Id { get; set; }
+ public string Email { get; set; }
+ public string Name { get; set; }
+ }
+
+ public class EnvironmentUrlDataDto
+ {
+ public string Base { get; set; }
+ public string Icons { get; set; }
+ }
+
+ //public class SettingsDataDto
+ //{
+ // public int? VaultTimeoutInMinutes { get; set; }
+
+ // public VaultTimeoutAction VaultTimeoutAction { get; set; }
+ //}
+ }
+}
diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs
index 4e95cf847..91f633b37 100644
--- a/src/Core/Services/AuthService.cs
+++ b/src/Core/Services/AuthService.cs
@@ -26,6 +26,8 @@ namespace Bit.Core.Services
private readonly IKeyConnectorService _keyConnectorService;
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly bool _setCryptoKeys;
+
+ private readonly LazyResolve _watchDeviceService = new LazyResolve();
private SymmetricCryptoKey _key;
public AuthService(
@@ -187,6 +189,7 @@ namespace Bit.Core.Services
{
callback.Invoke();
_messagingService.Send(AccountsManagerMessageCommands.LOGGED_OUT);
+ _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget();
}
public List GetSupportedTwoFactorProviders()
diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs
index bf48e80d8..e289a1ae6 100644
--- a/src/Core/Services/CipherService.cs
+++ b/src/Core/Services/CipherService.cs
@@ -226,7 +226,7 @@ namespace Bit.Core.Services
return response?.ToList() ?? new List();
}
- public async Task> GetAllDecryptedAsync()
+ public async Task> GetAllDecryptedAsync(Func filter = null)
{
if (_clearCipherCacheKey != null)
{
@@ -237,7 +237,7 @@ namespace Bit.Core.Services
await _storageService.RemoveAsync(_clearCipherCacheKey);
}
}
- if (DecryptedCipherCache != null)
+ if (DecryptedCipherCache != null && filter is null)
{
return DecryptedCipherCache;
}
@@ -261,13 +261,24 @@ namespace Bit.Core.Services
decCiphers.Add(c);
}
var tasks = new List();
- var ciphers = await GetAllAsync();
+ IEnumerable ciphers = await GetAllAsync();
+ if (filter != null)
+ {
+ ciphers = ciphers.Where(filter);
+ }
+
foreach (var cipher in ciphers)
{
tasks.Add(decryptAndAddCipherAsync(cipher));
}
await Task.WhenAll(tasks);
decCiphers = decCiphers.OrderBy(c => c, new CipherLocaleComparer(_i18nService)).ToList();
+
+ if (filter != null)
+ {
+ return decCiphers;
+ }
+
DecryptedCipherCache = decCiphers;
return DecryptedCipherCache;
}
diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs
index e13efadd6..72da24ff0 100644
--- a/src/Core/Services/StateService.cs
+++ b/src/Core/Services/StateService.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
@@ -52,6 +51,15 @@ namespace Bit.Core.Services
return await GetEmailAsync(activeUserId);
}
+ public async Task GetActiveUserCustomDataAsync(Func dataMapper)
+ {
+ var userId = await GetActiveUserIdAsync();
+ var account = await GetAccountAsync(
+ ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync())
+ );
+ return dataMapper(account);
+ }
+
public async Task IsActiveAccountAsync(string userId = null)
{
if (userId == null)
@@ -1685,5 +1693,21 @@ namespace Bit.Core.Services
}
throw new Exception("User does not exist in account list");
}
+
+ public async Task GetShouldConnectToWatchAsync(string userId = null)
+ {
+ var reconciledOptions =
+ ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync());
+ var key = Constants.ShouldConnectToWatchKey(reconciledOptions.UserId);
+ return await GetValueAsync(key, reconciledOptions) ?? false;
+ }
+
+ public async Task SetShouldConnectToWatchAsync(bool shouldConnect, string userId = null)
+ {
+ var reconciledOptions =
+ ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync());
+ var key = Constants.ShouldConnectToWatchKey(reconciledOptions.UserId);
+ await SetValueAsync(key, shouldConnect, reconciledOptions);
+ }
}
}
diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs
index cf3db3a7e..7eeaeda31 100644
--- a/src/Core/Services/SyncService.cs
+++ b/src/Core/Services/SyncService.cs
@@ -27,6 +27,8 @@ namespace Bit.Core.Services
private readonly ILogger _logger;
private readonly Func, Task> _logoutCallbackAsync;
+ private readonly LazyResolve _watchDeviceService = new LazyResolve();
+
public SyncService(
IStateService stateService,
IApiService apiService,
@@ -112,6 +114,8 @@ namespace Bit.Core.Services
await SyncPoliciesAsync(response.Policies);
await SyncSendsAsync(userId, response.Sends);
await SetLastSyncAsync(now);
+ _watchDeviceService.Value.SyncDataToWatchAsync().FireAndForget();
+
return SyncCompleted(true);
}
catch
diff --git a/src/iOS.Core/Services/WatchDeviceService.cs b/src/iOS.Core/Services/WatchDeviceService.cs
new file mode 100644
index 000000000..89fda8da9
--- /dev/null
+++ b/src/iOS.Core/Services/WatchDeviceService.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Bit.App.Services;
+using Bit.Core.Abstractions;
+using Bit.Core.Models;
+using Newtonsoft.Json;
+using WatchConnectivity;
+
+namespace Bit.iOS.Core.Services
+{
+ public class WatchDeviceService : BaseWatchDeviceService
+ {
+ public WatchDeviceService(ICipherService cipherService,
+ IEnvironmentService environmentService,
+ IStateService stateService,
+ IVaultTimeoutService vaultTimeoutService)
+ : base(cipherService, environmentService, stateService, vaultTimeoutService)
+ {
+ }
+
+ public override bool IsConnected => WCSessionManager.SharedManager.IsSessionActivated;
+
+ protected override bool CanSendData => WCSessionManager.SharedManager.IsValidSession;
+
+ protected override bool IsSupported => WCSession.IsSupported;
+
+ protected override Task SendDataToWatchAsync(WatchDTO watchDto)
+ {
+ var serializedData = JsonConvert.SerializeObject(watchDto);
+
+ // Add time to the key to make it change on every message sent so it's delivered faster.
+ // If we use the same key then the OS may defer the delivery of the message because of
+ // resources, reachability and other stuff
+ WCSessionManager.SharedManager.SendBackgroundHighPriorityMessage(new Dictionary
+ {
+ [$"watchDto-{DateTime.UtcNow.ToLongTimeString()}"] = serializedData
+ });
+
+ return Task.CompletedTask;
+ }
+
+ protected override void ConnectToWatch()
+ {
+ WCSessionManager.SharedManager.StartSession();
+ }
+ }
+}
diff --git a/src/iOS.Core/Utilities/DictionaryExtensions.cs b/src/iOS.Core/Utilities/DictionaryExtensions.cs
new file mode 100644
index 000000000..38379209a
--- /dev/null
+++ b/src/iOS.Core/Utilities/DictionaryExtensions.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Foundation;
+using Newtonsoft.Json;
+
+namespace Bit.iOS.Core.Utilities
+{
+ public static class DictionaryExtensions
+ {
+ public static NSDictionary ToNSDictionary(this Dictionary dict)
+ {
+ return dict.ToNSDictionary(k => new NSString(k), v => (NSObject)new NSString(JsonConvert.SerializeObject(v)));
+ }
+
+ public static NSDictionary ToNSDictionary(this Dictionary dict, Func keyConverter, Func valueConverter)
+ where KTo : NSObject
+ where VTo : NSObject
+ {
+ var NSValues = dict.Values.Select(x => valueConverter(x)).ToArray();
+ var NSKeys = dict.Keys.Select(x => keyConverter(x)).ToArray();
+ return NSDictionary.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count());
+ }
+ }
+}
+
diff --git a/src/iOS.Core/Utilities/WCSessionManager.cs b/src/iOS.Core/Utilities/WCSessionManager.cs
new file mode 100644
index 000000000..34cb8b254
--- /dev/null
+++ b/src/iOS.Core/Utilities/WCSessionManager.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Bit.iOS.Core.Utilities;
+using Foundation;
+using Newtonsoft.Json;
+
+namespace WatchConnectivity
+{
+ public sealed class WCSessionManager : WCSessionDelegate
+ {
+ // Setup is converted from https://www.natashatherobot.com/watchconnectivity-say-hello-to-wcsession/
+ // with some extra bits
+ private static readonly WCSessionManager sharedManager = new WCSessionManager();
+ private static WCSession session = WCSession.IsSupported ? WCSession.DefaultSession : null;
+
+ public static string Device = "Phone";
+
+ public event WCSessionReceiveDataHandler ApplicationContextUpdated;
+ public event WCSessionReceiveDataHandler MessagedReceived;
+ public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary applicationContext);
+
+
+ private WCSession validSession
+ {
+ get
+ {
+ Console.WriteLine($"Paired status:{(session.Paired ? '✓' : '✗')}\n");
+ Console.WriteLine($"Watch App Installed status:{(session.WatchAppInstalled ? '✓' : '✗')}\n");
+ return (session.Paired && session.WatchAppInstalled) ? session : null;
+ }
+ }
+
+ private WCSession validReachableSession
+ {
+ get
+ {
+ return session.Reachable ? validSession : null;
+ }
+ }
+
+ public bool IsValidSession => validSession != null;
+
+ public bool IsSessionReachable => session.Reachable;
+
+ public bool IsSessionActivated => validSession?.ActivationState == WCSessionActivationState.Activated;
+
+ private WCSessionManager() : base() { }
+
+ public static WCSessionManager SharedManager
+ {
+ get
+ {
+ return sharedManager;
+ }
+ }
+
+ public void StartSession()
+ {
+ if (session != null)
+ {
+ session.Delegate = this;
+ session.ActivateSession();
+ Console.WriteLine($"Started Watch Connectivity Session on {Device}");
+ }
+ }
+
+ public override void SessionReachabilityDidChange(WCSession session)
+ {
+ Console.WriteLine($"Watch connectivity Reachable:{(session.Reachable ? '✓' : '✗')} from {Device}");
+ // handle session reachability change
+ if (session.Reachable)
+ {
+ // great! continue on with Interactive Messaging
+ }
+ else
+ {
+ // 😥 prompt the user to unlock their iOS device
+ }
+ }
+
+ #region Application Context Methods
+
+ public void SendBackgroundHighPriorityMessage(Dictionary applicationContext)
+ {
+ // Application context doesnt need the watch to be reachable, it will be received when opened
+ if (validSession is null || validSession.ActivationState != WCSessionActivationState.Activated)
+ {
+ return;
+ }
+
+ Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
+ {
+ try
+ {
+ var sendSuccessfully = validSession.UpdateApplicationContext(applicationContext.ToNSDictionary(), out var error);
+ if (sendSuccessfully)
+ {
+ Console.WriteLine($"Sent App Context from {Device} \nPayLoad: {applicationContext.ToNSDictionary().ToString()} \n");
+ }
+ else
+ {
+ Console.WriteLine($"Error Updating Application Context: {error.LocalizedDescription}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Exception Updating Application Context: {ex.Message}");
+ }
+ });
+ }
+ WCSessionUserInfoTransfer _transf;
+ public void SendBackgroundFifoHighPriorityMessage(Dictionary message)
+ {
+ if(validSession is null || validSession.ActivationState != WCSessionActivationState.Activated)
+ {
+ return;
+ }
+
+ _transf?.Cancel();
+
+ Console.WriteLine("Started transferring user info");
+
+ _transf = session.TransferUserInfo(message.ToNSDictionary());
+
+
+ Task.Run(async () =>
+ {
+ try
+ {
+ while (_transf.Transferring)
+ {
+ await Task.Delay(1000);
+ }
+ Console.WriteLine("Finished transferring user info");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Error transferring user info " + ex);
+ }
+ });
+
+ //session.SendMessage(dic,
+ // (dd) =>
+ // {
+ // Console.WriteLine(dd?.ToString());
+ // },
+ // error =>
+ // {
+ // Console.WriteLine(error?.ToString());
+ // }
+ //);
+ }
+
+ public override void DidReceiveApplicationContext(WCSession session, NSDictionary applicationContext)
+ {
+ Console.WriteLine($"Receiving Message on {Device}");
+ if (ApplicationContextUpdated != null)
+ {
+ var keys = applicationContext.Keys.Select(k => k.ToString()).ToArray();
+ var values = applicationContext.Values.Select(v => JsonConvert.DeserializeObject(v.ToString())).ToArray();
+ var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v })
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ ApplicationContextUpdated(session, dictionary);
+ }
+ }
+
+
+ public override void DidReceiveMessage(WCSession session, NSDictionary message)
+ {
+ Console.WriteLine($"Receiving Message on {Device}");
+
+ var keys = message.Keys.Select(k => k.ToString()).ToArray();
+ var values = message.Values.Select(v => v?.ToString() as object).ToArray();
+ var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v })
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ MessagedReceived?.Invoke(session, dictionary);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs
index 4d6a13c6c..41afc0bfc 100644
--- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs
+++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs
@@ -48,6 +48,12 @@ namespace Bit.iOS.Core.Utilities
clearCipherCacheKey,
Bit.Core.Constants.iOSAllClearCipherCacheKeys);
InitLogger();
+
+ ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve()));
+
Bootstrap();
var appOptions = new AppOptions { IosExtension = true };
@@ -226,7 +232,8 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Resolve("platformUtilsService"),
ServiceContainer.Resolve("authService"),
ServiceContainer.Resolve("logger"),
- ServiceContainer.Resolve("messagingService"));
+ ServiceContainer.Resolve("messagingService"),
+ ServiceContainer.Resolve());
ServiceContainer.Register("accountsManager", accountsManager);
if (postBootstrapFunc != null)
diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj
index a4515c9b8..f52468e43 100644
--- a/src/iOS.Core/iOS.Core.csproj
+++ b/src/iOS.Core/iOS.Core.csproj
@@ -204,9 +204,12 @@
+
+
+
diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs
index 25b9049e2..ef1544ea8 100644
--- a/src/iOS/AppDelegate.cs
+++ b/src/iOS/AppDelegate.cs
@@ -11,12 +11,13 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
+using Bit.iOS.Core.Services;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Services;
using CoreNFC;
using Foundation;
using UIKit;
-using UserNotifications;
+using WatchConnectivity;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
@@ -57,6 +58,9 @@ namespace Bit.iOS
LoadApplication(new App.App(null));
iOSCoreHelpers.AppearanceAdjustments();
ZXing.Net.Mobile.Forms.iOS.Platform.Init();
+
+ ConnectToWatchIfNeededAsync().FireAndForget();
+
_broadcasterService.Subscribe(nameof(AppDelegate), async (message) =>
{
try
@@ -302,6 +306,12 @@ namespace Bit.iOS
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey,
Constants.iOSAllClearCipherCacheKeys);
iOSCoreHelpers.InitLogger();
+
+ ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve()));
+
_pushHandler = new iOSPushNotificationHandler(
ServiceContainer.Resolve("pushNotificationListenerService"));
_nfcDelegate = new Core.NFCReaderDelegate((success, message) =>
@@ -393,5 +403,13 @@ namespace Bit.iOS
await AppHelpers.SetPreconfiguredSettingsAsync(dict);
}
}
+
+ private async Task ConnectToWatchIfNeededAsync()
+ {
+ if (_stateService != null && await _stateService.GetShouldConnectToWatchAsync())
+ {
+ WCSessionManager.SharedManager.StartSession();
+ }
+ }
}
}
diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj
index 0e85b0d9e..600697876 100644
--- a/src/iOS/iOS.csproj
+++ b/src/iOS/iOS.csproj
@@ -134,6 +134,15 @@
+
+ $(Home)/Library/Developer/Xcode/DerivedData/bitwarden-cbtqsueryycvflfzbsoteofskiyr/Build/Products
+ $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..'))/watchOS/bitwarden.xcarchive/Products/Applications/bitwarden.app/Watch
+ Bitwarden.app
+ watchsimulator
+ watchos
+ $(WatchAppBuildPath)/$(Configuration)-$(WatchAppConfiguration)/$(WatchAppBundle)
+ $(WatchAppBuildPath)/$(WatchAppBundle)
+
@@ -406,4 +415,17 @@
+
+ <_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" />
+
+
+ <_ResolvedWatchAppReferences Include="$(WatchAppBundleFullPath)" />
+
+
+ --deep
+
+
+
+
+
\ No newline at end of file
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..14b11821c
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.349",
+ "green" : "0.664",
+ "red" : "0.279"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png
new file mode 100644
index 000000000..eb753fbce
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/102.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/102.png
new file mode 100644
index 000000000..7050b6268
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/102.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/172.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/172.png
new file mode 100644
index 000000000..27d94caf6
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/172.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/196.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/196.png
new file mode 100644
index 000000000..e6eccd281
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/196.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/216.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/216.png
new file mode 100644
index 000000000..26d476a10
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/216.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/234.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/234.png
new file mode 100644
index 000000000..7251b0ea6
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/234.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/48.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/48.png
new file mode 100644
index 000000000..dfa17fa78
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/48.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/55.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/55.png
new file mode 100644
index 000000000..9b2005583
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/55.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/66.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/66.png
new file mode 100644
index 000000000..66f36732d
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/66.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/88.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/88.png
new file mode 100644
index 000000000..7719b7a17
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/88.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/92.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/92.png
new file mode 100644
index 000000000..6ba8d0d57
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/92.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..9d947cd9b
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,138 @@
+{
+ "images" : [
+ {
+ "filename" : "48.png",
+ "idiom" : "watch",
+ "role" : "notificationCenter",
+ "scale" : "2x",
+ "size" : "24x24",
+ "subtype" : "38mm"
+ },
+ {
+ "filename" : "55.png",
+ "idiom" : "watch",
+ "role" : "notificationCenter",
+ "scale" : "2x",
+ "size" : "27.5x27.5",
+ "subtype" : "42mm"
+ },
+ {
+ "filename" : "Icon-59.png",
+ "idiom" : "watch",
+ "role" : "companionSettings",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "Icon-87.png",
+ "idiom" : "watch",
+ "role" : "companionSettings",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "66.png",
+ "idiom" : "watch",
+ "role" : "notificationCenter",
+ "scale" : "2x",
+ "size" : "33x33",
+ "subtype" : "45mm"
+ },
+ {
+ "filename" : "Icon-80.png",
+ "idiom" : "watch",
+ "role" : "appLauncher",
+ "scale" : "2x",
+ "size" : "40x40",
+ "subtype" : "38mm"
+ },
+ {
+ "filename" : "88.png",
+ "idiom" : "watch",
+ "role" : "appLauncher",
+ "scale" : "2x",
+ "size" : "44x44",
+ "subtype" : "40mm"
+ },
+ {
+ "filename" : "92.png",
+ "idiom" : "watch",
+ "role" : "appLauncher",
+ "scale" : "2x",
+ "size" : "46x46",
+ "subtype" : "41mm"
+ },
+ {
+ "filename" : "100.png",
+ "idiom" : "watch",
+ "role" : "appLauncher",
+ "scale" : "2x",
+ "size" : "50x50",
+ "subtype" : "44mm"
+ },
+ {
+ "filename" : "102.png",
+ "idiom" : "watch",
+ "role" : "appLauncher",
+ "scale" : "2x",
+ "size" : "51x51",
+ "subtype" : "45mm"
+ },
+ {
+ "idiom" : "watch",
+ "role" : "appLauncher",
+ "scale" : "2x",
+ "size" : "54x54",
+ "subtype" : "49mm"
+ },
+ {
+ "filename" : "172.png",
+ "idiom" : "watch",
+ "role" : "quickLook",
+ "scale" : "2x",
+ "size" : "86x86",
+ "subtype" : "38mm"
+ },
+ {
+ "filename" : "196.png",
+ "idiom" : "watch",
+ "role" : "quickLook",
+ "scale" : "2x",
+ "size" : "98x98",
+ "subtype" : "42mm"
+ },
+ {
+ "filename" : "216.png",
+ "idiom" : "watch",
+ "role" : "quickLook",
+ "scale" : "2x",
+ "size" : "108x108",
+ "subtype" : "44mm"
+ },
+ {
+ "filename" : "234.png",
+ "idiom" : "watch",
+ "role" : "quickLook",
+ "scale" : "2x",
+ "size" : "117x117",
+ "subtype" : "45mm"
+ },
+ {
+ "idiom" : "watch",
+ "role" : "quickLook",
+ "scale" : "2x",
+ "size" : "129x129",
+ "subtype" : "49mm"
+ },
+ {
+ "filename" : "Icon-1024.png",
+ "idiom" : "watch-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
new file mode 100644
index 000000000..22db2584c
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-1024.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-59.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-59.png
new file mode 100644
index 000000000..2f3bf8af1
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-59.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-80.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-80.png
new file mode 100644
index 000000000..01e5a4ae4
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-80.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-87.png b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-87.png
new file mode 100644
index 000000000..5642c2028
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-87.png differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit App/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/Contents.json
new file mode 100644
index 000000000..0fcd05af3
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "logo-horizontal-blue (2) 3.pdf",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "logo-horizontal-blue (2) 2.pdf",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "logo-horizontal-blue (2) 1.pdf",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 1.pdf b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 1.pdf
new file mode 100644
index 000000000..f68f5dade
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 1.pdf differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 2.pdf b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 2.pdf
new file mode 100644
index 000000000..f68f5dade
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 2.pdf differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 3.pdf b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 3.pdf
new file mode 100644
index 000000000..f68f5dade
Binary files /dev/null and b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/BitwardenImagetype.imageset/logo-horizontal-blue (2) 3.pdf differ
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json
new file mode 100644
index 000000000..26454cac8
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : "<=145"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json
new file mode 100644
index 000000000..e8b3252e3
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json
@@ -0,0 +1,53 @@
+{
+ "assets" : [
+ {
+ "filename" : "Circular.imageset",
+ "idiom" : "watch",
+ "role" : "circular"
+ },
+ {
+ "filename" : "Extra Large.imageset",
+ "idiom" : "watch",
+ "role" : "extra-large"
+ },
+ {
+ "filename" : "Graphic Bezel.imageset",
+ "idiom" : "watch",
+ "role" : "graphic-bezel"
+ },
+ {
+ "filename" : "Graphic Circular.imageset",
+ "idiom" : "watch",
+ "role" : "graphic-circular"
+ },
+ {
+ "filename" : "Graphic Corner.imageset",
+ "idiom" : "watch",
+ "role" : "graphic-corner"
+ },
+ {
+ "filename" : "Graphic Extra Large.imageset",
+ "idiom" : "watch",
+ "role" : "graphic-extra-large"
+ },
+ {
+ "filename" : "Graphic Large Rectangular.imageset",
+ "idiom" : "watch",
+ "role" : "graphic-large-rectangular"
+ },
+ {
+ "filename" : "Modular.imageset",
+ "idiom" : "watch",
+ "role" : "modular"
+ },
+ {
+ "filename" : "Utilitarian.imageset",
+ "idiom" : "watch",
+ "role" : "utilitarian"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json
new file mode 100644
index 000000000..26454cac8
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : "<=145"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json
new file mode 100644
index 000000000..6e184db8f
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json
new file mode 100644
index 000000000..6e184db8f
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json
new file mode 100644
index 000000000..6e184db8f
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json
new file mode 100644
index 000000000..26454cac8
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : "<=145"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json
new file mode 100644
index 000000000..6e184db8f
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json
new file mode 100644
index 000000000..26454cac8
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : "<=145"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json
new file mode 100644
index 000000000..26454cac8
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "idiom" : "watch",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : "<=145"
+ },
+ {
+ "idiom" : "watch",
+ "scale" : "2x",
+ "screen-width" : ">183"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "auto-scaling" : "auto"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DarkTextMuted.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DarkTextMuted.colorset/Contents.json
new file mode 100644
index 000000000..d5dac6ad3
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DarkTextMuted.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xCE",
+ "green" : "0xC0",
+ "red" : "0xBA"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/Contents.json
new file mode 100644
index 000000000..a6254502d
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "globe 1.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "globe.svg",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "globe 2.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 1.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 1.svg
new file mode 100644
index 000000000..f0f5ed288
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 1.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 2.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 2.svg
new file mode 100644
index 000000000..f0f5ed288
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe 2.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe.svg
new file mode 100644
index 000000000..f0f5ed288
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/DefaultCipherIcon.imageset/globe.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/Contents.json
new file mode 100644
index 000000000..0d301e528
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "emptystatedark.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "emptystatedark 1.svg",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "emptystatedark 2.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 1.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 1.svg
new file mode 100644
index 000000000..f0c83d064
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 1.svg
@@ -0,0 +1,22 @@
+
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 2.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 2.svg
new file mode 100644
index 000000000..f0c83d064
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark 2.svg
@@ -0,0 +1,22 @@
+
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark.svg b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark.svg
new file mode 100644
index 000000000..f0c83d064
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/EmptyListPlaceholder.imageset/emptystatedark.svg
@@ -0,0 +1,22 @@
+
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/ItemBackground.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/ItemBackground.colorset/Contents.json
new file mode 100644
index 000000000..cbecde52e
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Assets.xcassets/ItemBackground.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "38",
+ "green" : "28",
+ "red" : "22"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/ComplicationController.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ComplicationController.swift
new file mode 100644
index 000000000..c369727a6
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ComplicationController.swift
@@ -0,0 +1,52 @@
+import ClockKit
+
+
+class ComplicationController: NSObject, CLKComplicationDataSource {
+
+ // MARK: - Complication Configuration
+
+ func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
+ let descriptors = [
+ CLKComplicationDescriptor(identifier: "complication", displayName: "bitwarden", supportedFamilies: CLKComplicationFamily.allCases)
+ // Multiple complication support can be added here with more descriptors
+ ]
+
+ // Call the handler with the currently supported complication descriptors
+ handler(descriptors)
+ }
+
+ func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) {
+ // Do any necessary work to support these newly shared complication descriptors
+ }
+
+ // MARK: - Timeline Configuration
+
+ func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
+ // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines
+ handler(nil)
+ }
+
+ func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
+ // Call the handler with your desired behavior when the device is locked
+ handler(.showOnLockScreen)
+ }
+
+ // MARK: - Timeline Population
+
+ func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
+ // Call the handler with the current timeline entry
+ handler(nil)
+ }
+
+ func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
+ // Call the handler with the timeline entries after the given date
+ handler(nil)
+ }
+
+ // MARK: - Sample Templates
+
+ func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
+ // This method will be called once per supported complication, and the results will be cached
+ handler(nil)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/AvatarView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/AvatarView.swift
new file mode 100644
index 000000000..e31bc6139
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/AvatarView.swift
@@ -0,0 +1,85 @@
+import SwiftUI
+
+struct AvatarView: View {
+ var circleColor = Color.white
+ var textColor = Color.black
+ var initials = ""
+
+ init(_ user: User?) {
+ let source = user?.name ?? user?.email
+ var upperCaseText: String? = nil
+
+ if source == nil || source!.isEmpty {
+ initials = ".."
+ } else if source!.count > 1 {
+ upperCaseText = source!.uppercased()
+ initials = getFirstLetters(upperCaseText!, 2)
+ } else {
+ upperCaseText = source!.uppercased()
+ initials = upperCaseText!
+ }
+
+ circleColor = stringToColor(str: user?.id ?? upperCaseText, fallbackColor: Color(hex: "#FFFFFF33")!)
+ textColor = textColorFromBgColor(circleColor)
+ }
+
+ var body: some View {
+ ZStack {
+ Circle()
+ .foregroundColor(circleColor)
+ .frame(width: 30, height: 30)
+ Text(initials)
+ .font(.footnote)
+ .foregroundColor(textColor)
+ }
+ }
+
+ func stringToColor(str: String?, fallbackColor: Color) -> Color {
+ guard let str = str else {
+ return fallbackColor
+ }
+
+ var hash = 0
+ for char in str {
+ let uniSca = String(char).unicodeScalars
+ let intCharValue = Int(uniSca[uniSca.startIndex].value)
+
+ hash = intCharValue + ((hash << 5) &- hash)
+ }
+ var color = "#"
+ for i in 0..<3 {
+ let value = (hash >> (i * 8)) & 0xff
+ color += String(value, radix: 16).leftPadding(toLength: 2, withPad: "0")
+ }
+ return Color(hex: color) ?? fallbackColor
+ }
+
+ func textColorFromBgColor(_ bgColor: Color, threshold: CGFloat = 0.65) -> Color {
+ let (r, g, b, _) = bgColor.components
+ let luminance = r * 0.299 + g * 0.587 + b * 0.114;
+ return luminance > threshold ? Color.black : Color.white;
+ }
+
+ func getFirstLetters(_ data: String, _ charCount: Int) -> String {
+ let sanitizedData = data.trimmingCharacters(in: CharacterSet.whitespaces)
+ let parts = sanitizedData.split(separator: " ")
+
+ if parts.count > 1 && charCount <= 2 {
+ var text = "";
+ for i in 0.. 2 {
+ return String(sanitizedData.prefix(2))
+ }
+ return sanitizedData;
+ }
+}
+
+struct AvatarView_Previews: PreviewProvider {
+ static var previews: some View {
+ AvatarView(User(id: "zxc", email: "asdfasdf@gmail.com", name: "John Snow"))
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/CircularProgressView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/CircularProgressView.swift
new file mode 100644
index 000000000..791e7a73d
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/CircularProgressView.swift
@@ -0,0 +1,39 @@
+import SwiftUI
+
+struct CircularProgressView: View {
+ let progress: Double
+ let strokeLineWidth:CGFloat
+ let strokeColor:Color
+ let endingStrokeColor:Color
+
+ var currentColor: Color{
+ return progress > 0.2 ? strokeColor : endingStrokeColor
+ }
+
+ var body: some View {
+ ZStack {
+ Circle()
+ .stroke(
+ currentColor.opacity(0.5),
+ lineWidth: strokeLineWidth
+ )
+ Circle()
+ .trim(from: 0, to: progress)
+ .stroke(
+ currentColor,
+ style: StrokeStyle(
+ lineWidth: strokeLineWidth,
+ lineCap: .round
+ )
+ )
+ .rotationEffect(.degrees(-90))
+ .animation(.easeOut, value: progress)
+ }
+ }
+}
+
+struct CircularProgressView_Previews: PreviewProvider {
+ static var previews: some View {
+ CircularProgressView(progress:0.5, strokeLineWidth:5, strokeColor: Color.blue, endingStrokeColor: Color.red)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/ImageView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/ImageView.swift
new file mode 100644
index 000000000..05467f330
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/ImageView.swift
@@ -0,0 +1,112 @@
+import Foundation
+import SwiftUI
+import Combine
+
+/// Image view to be used on watchOS < 8
+///
+/// - Note: Based on: https://stackoverflow.com/questions/60710997/images-disappear-in-list-as-i-scroll-swiftui-swift
+///
+struct ImageView: View {
+ @ObservedObject var imageLoader:ImageLoader
+ var imgMaxWidth:CGFloat
+ var imgMaxHeight:CGFloat
+ var placeholder: PlaceholderView
+
+ init(withURL url:String, maxWidth mw: CGFloat, maxHeight mh: CGFloat, @ViewBuilder _ placeholder: () -> PlaceholderView) {
+ imageLoader = ImageLoader(urlString:url)
+ self.imgMaxWidth = mw
+ self.imgMaxHeight = mh
+ self.placeholder = placeholder()
+ }
+
+ var body: some View {
+ if imageLoader.image == nil {
+ placeholder
+ } else {
+ Image(uiImage: imageLoader.image! )
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(maxWidth:imgMaxWidth, maxHeight:imgMaxHeight)
+ }
+ }
+}
+
+class ImageLoader: ObservableObject {
+ @Published var image: UIImage?
+ var urlString: String?
+ var imageCache = ImageCache.getImageCache()
+
+ init(urlString: String?) {
+ self.urlString = urlString
+ loadImage()
+ }
+
+ func loadImage() {
+ if loadImageFromCache() {
+ return
+ }
+
+ loadImageFromUrl()
+ }
+
+ func loadImageFromCache() -> Bool {
+ guard let urlString = urlString else {
+ return false
+ }
+
+ guard let cacheImage = imageCache.get(forKey: urlString) else {
+ return false
+ }
+
+ image = cacheImage
+ return true
+ }
+
+ func loadImageFromUrl() {
+ guard let urlString = urlString else {
+ return
+ }
+
+ let url = URL(string: urlString)!
+ let task = URLSession.shared.dataTask(with: url, completionHandler: getImageFromResponse(data:response:error:))
+ task.resume()
+ }
+
+
+ func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?) {
+ guard error == nil else {
+ return
+ }
+ guard let data = data else {
+ return
+ }
+
+ DispatchQueue.main.async {
+ guard let loadedImage = UIImage(data: data) else {
+ return
+ }
+
+ self.imageCache.set(forKey: self.urlString!, image: loadedImage)
+ self.image = loadedImage
+ }
+ }
+}
+
+class ImageCache {
+ var cache = NSCache()
+
+ func get(forKey: String) -> UIImage? {
+ return cache.object(forKey: NSString(string: forKey))
+ }
+
+ func set(forKey: String, image: UIImage) {
+ cache.setObject(image, forKey: NSString(string: forKey))
+ }
+}
+
+extension ImageCache {
+ private static var imageCache = ImageCache()
+ static func getImageCache() -> ImageCache {
+ return imageCache
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/TrackableWithHeaderListView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/TrackableWithHeaderListView.swift
new file mode 100644
index 000000000..f6c1a147e
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Controls/TrackableWithHeaderListView.swift
@@ -0,0 +1,40 @@
+import Foundation
+import SwiftUI
+
+/// List that has offset tracking and a header
+///
+/// - Note: Based on: https://stackoverflow.com/questions/74047146/tracking-scroll-position-in-a-list-swiftui
+///
+struct TrackableWithHeaderListView: View {
+ let offsetChanged: (CGPoint?) -> Void
+ let headerContent: HeaderContent
+ let content: Content
+
+ init(offsetChanged: @escaping (CGPoint?) -> Void = { _ in }, @ViewBuilder headerContent: () -> HeaderContent, @ViewBuilder content: () -> Content) {
+ self.offsetChanged = offsetChanged
+ self.headerContent = headerContent()
+ self.content = content()
+ }
+
+ var body: some View {
+ List {
+ GeometryReader { geometry in
+ headerContent
+ .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("ListView")).origin)
+ }
+ .frame(width: .infinity)
+ content
+ }
+ .coordinateSpace(name: "ListView")
+ .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)
+ }
+}
+
+private struct ScrollOffsetPreferenceKey: PreferenceKey {
+ static var defaultValue: CGPoint? = nil
+ static func reduce(value: inout CGPoint?, nextValue: () -> CGPoint?) {
+ if let nextValue = nextValue() {
+ value = nextValue
+ }
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/CoreDataHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/CoreDataHelper.swift
new file mode 100644
index 000000000..3c51d2524
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/CoreDataHelper.swift
@@ -0,0 +1,127 @@
+import Foundation
+import CoreData
+
+// Based on https://medium.com/swlh/using-core-data-in-your-swiftui-app-with-combine-mvvm-and-protocols-4577f44d240d
+
+class CoreDataHelper: DBHelperProtocol {
+ static let shared = CoreDataHelper()
+
+ typealias ObjectType = NSManagedObject
+ typealias PredicateType = NSPredicate
+
+ var context: NSManagedObjectContext { persistentContainer.viewContext }
+
+ // MARK: - Core Data
+
+ lazy var persistentContainer: NSPersistentContainer = {
+ StringEncryptionTransformer.register()
+ let container = NSPersistentContainer(name: "BitwardenDB")
+
+ container.loadPersistentStores(completionHandler: { (storeDescription, error) in
+ if let error = error as NSError? {
+ fatalError("Unresolved error \(error), \(error.userInfo)")
+ }
+ })
+ return container
+ }()
+
+ func saveContext () {
+ let context = persistentContainer.viewContext
+ if context.hasChanges {
+ do {
+ try context.save()
+ } catch {
+ let nserror = error as NSError
+ fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
+ }
+ }
+ }
+
+ // MARK: - DBHelper Protocol
+
+
+ func create(_ object: NSManagedObject) {
+ do {
+ try context.save()
+ } catch {
+ fatalError("error saving context while creating an object")
+ }
+ }
+
+ func fetch(_ objectType: T.Type, _ entityName: String, predicate: NSPredicate? = nil, limit: Int? = nil) -> Result<[T], Error> {
+ let request = NSFetchRequest(entityName: entityName)
+ request.predicate = predicate
+ if let limit = limit {
+ request.fetchLimit = limit
+ }
+ do {
+ let result = try context.fetch(request)
+ return .success(result as [T])
+ } catch {
+ return .failure(error)
+ }
+ }
+
+ func fetchFirst(_ objectType: T.Type, predicate: NSPredicate?) -> Result {
+ let result = fetch(objectType, predicate: predicate, limit: 1)
+ switch result {
+ case .success(let todos):
+ return .success(todos.first as? T)
+ case .failure(let error):
+ return .failure(error)
+ }
+ }
+
+ func update(_ object: NSManagedObject) {
+ do {
+ try context.save()
+ } catch {
+ fatalError("error saving context while updating an object")
+ }
+ }
+
+ func delete(_ object: NSManagedObject) {
+ context.delete(object)
+ }
+
+ func insertBatch(_ entityName: String, items: [Any], itemMapper: @escaping (Any, NSManagedObjectContext) -> [String : Any], completionHandler: @escaping () -> Void) {
+ self.persistentContainer.performBackgroundTask { context in
+ context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
+ let objects = items.map { item in
+ itemMapper(item, context)
+ }
+ let batchInsert = NSBatchInsertRequest(entityName: entityName, objects: objects)
+ batchInsert.resultType = NSBatchInsertRequestResultType.objectIDs
+ do {
+ let result = try context.execute(batchInsert) as! NSBatchInsertResult
+ if let objectIDs = result.result as? [NSManagedObjectID], !objectIDs.isEmpty {
+ let save = [NSInsertedObjectsKey: objectIDs]
+ NSManagedObjectContext.mergeChanges(fromRemoteContextSave: save, into: [self.context])
+ }
+ }
+ catch let nsError as NSError {
+ fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+ DispatchQueue.main.async {
+ completionHandler()
+ }
+ }
+ }
+
+ func deleteAll(_ entityName: String, predicate: NSPredicate? = nil, completionHandler: @escaping () -> Void) {
+ let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: entityName)
+ fetchRequest.predicate = predicate
+ let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
+ deleteRequest.resultType = .resultTypeObjectIDs
+
+ self.persistentContainer.performBackgroundTask { context in
+ do {
+ try context.execute(deleteRequest)
+ } catch let nsError as NSError {
+ Log.e("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+
+ completionHandler()
+ }
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/DBHelperProtocol.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/DBHelperProtocol.swift
new file mode 100644
index 000000000..db482194b
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/DataStorage/DBHelperProtocol.swift
@@ -0,0 +1,20 @@
+import Foundation
+import CoreData
+
+public protocol DBHelperProtocol {
+ associatedtype ObjectType
+ associatedtype PredicateType
+
+ func create(_ object: ObjectType)
+ func fetchFirst(_ objectType: ObjectType.Type, predicate: PredicateType?) -> Result
+ func fetch(_ objectType: ObjectType.Type, predicate: PredicateType?, limit: Int?) -> Result<[ObjectType], Error>
+ func update(_ object: ObjectType)
+ func delete(_ object: ObjectType)
+ func insertBatch(_ entityName: String, items: [Any], itemMapper: @escaping (Any, NSManagedObjectContext) -> [String : Any], completionHandler: @escaping () -> Void)
+}
+
+public extension DBHelperProtocol {
+ func fetch(_ objectType: ObjectType.Type, predicate: PredicateType? = nil, limit: Int? = nil) -> Result<[ObjectType], Error> {
+ return fetch(objectType, predicate: predicate, limit: limit)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/BitwardenDB.xcdatamodeld/BitwardenDB.xcdatamodel/contents b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/BitwardenDB.xcdatamodeld/BitwardenDB.xcdatamodel/contents
new file mode 100644
index 000000000..137e02a5c
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/BitwardenDB.xcdatamodeld/BitwardenDB.xcdatamodel/contents
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataClass.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataClass.swift
new file mode 100644
index 000000000..0631111a6
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataClass.swift
@@ -0,0 +1,43 @@
+import Foundation
+import CoreData
+
+enum DecoderConfigurationError: Error {
+ case missingManagedObjectContext
+}
+
+@objc(CipherEntity)
+public class CipherEntity: NSManagedObject, Codable {
+ enum CodingKeys: CodingKey {
+ case id, name, username, totp, loginUris, userId
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(id, forKey: .id)
+ try container.encode(name, forKey: .name)
+ try container.encode(userId, forKey: .userId)
+ try container.encode(username, forKey: .username)
+ try container.encode(totp, forKey: .totp)
+ try container.encode(loginUris, forKey: .loginUris)
+ }
+
+ public required convenience init(from decoder: Decoder) throws {
+ guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
+ throw DecoderConfigurationError.missingManagedObjectContext
+ }
+
+ self.init(context: context)
+
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.id = try container.decode(String.self, forKey: .id)
+ self.name = try container.decode(String?.self, forKey: .name)
+ self.userId = try container.decode(String.self, forKey: .userId)
+ self.username = try container.decode(String?.self, forKey: .username)
+ self.totp = try container.decode(String?.self, forKey: .totp)
+ self.loginUris = try container.decode(String?.self, forKey: .loginUris)
+ }
+}
+
+extension CodingUserInfoKey {
+ static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataProperties.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataProperties.swift
new file mode 100644
index 000000000..91bfd2d1f
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/CipherEntity+CoreDataProperties.swift
@@ -0,0 +1,34 @@
+import Foundation
+import CoreData
+
+
+extension CipherEntity {
+
+ @nonobjc public class func fetchRequest() -> NSFetchRequest {
+ return NSFetchRequest(entityName: "CipherEntity")
+ }
+
+ @NSManaged public var id: String
+ @NSManaged public var name: String?
+ @NSManaged public var userId: String
+ @NSManaged public var totp: String?
+ @NSManaged public var type: NSObject?
+ @NSManaged public var username: String?
+ @NSManaged public var loginUris: String?
+
+}
+
+extension CipherEntity : Identifiable {
+ func toCipher() -> Cipher{
+
+ var loginUrisArray: [LoginUri]?
+ if loginUris != nil {
+ loginUrisArray = try? JSONDecoder().decode([LoginUri].self, from: loginUris!.data(using: .utf8)!)
+ }
+
+ return Cipher(id: id,
+ name: name,
+ userId: userId,
+ login: Login(username: username, totp: totp, uris: loginUrisArray))
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/StringEncryptionTransformer.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/StringEncryptionTransformer.swift
new file mode 100644
index 000000000..3fae2d5e7
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Entities/StringEncryptionTransformer.swift
@@ -0,0 +1,48 @@
+import Foundation
+import UIKit
+
+@objc(StringEncryptionTransformer)
+class StringEncryptionTransformer : ValueTransformer {
+ var cryptoService: CryptoService = CryptoService()
+
+ override public class func allowsReverseTransformation() -> Bool {
+ return true
+ }
+
+ override func transformedValue(_ value: Any?) -> Any?{
+ var toEncrypt: String
+
+ switch value {
+ case let aString as String:
+ toEncrypt = aString
+ default:
+ return nil
+ }
+
+ if let encryptedData = cryptoService.encrypt(toEncrypt) {
+ return encryptedData
+ }
+
+ return nil
+ }
+
+ override func reverseTransformedValue(_ value: Any?) -> Any?{
+ if let encryptedData = value as? Data {
+ if let decryptedData = cryptoService.decrypt(encryptedData) {
+ return String(decoding: decryptedData, as: UTF8.self)
+ }
+ }
+
+ return nil
+ }
+}
+
+extension StringEncryptionTransformer {
+ static let name = NSValueTransformerName(rawValue: String(describing: StringEncryptionTransformer.self))
+
+ /// Registers the value transformer with `ValueTransformer`.
+ public static func register() {
+ let transformer = StringEncryptionTransformer()
+ ValueTransformer.setValueTransformer(transformer, forName: name)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/IconImageHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/IconImageHelper.swift
new file mode 100644
index 000000000..f25daeb03
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/IconImageHelper.swift
@@ -0,0 +1,38 @@
+import Foundation
+
+class IconImageHelper{
+ static let shared: IconImageHelper = IconImageHelper()
+
+ private init(){}
+
+ func getLoginIconImage(_ cipher:Cipher) -> String? {
+ guard let uris = cipher.login.uris, uris.count > 0 else {
+ return nil
+ }
+
+ for u in uris {
+ guard var hostname = u.uri, hostname.contains(".") else {
+ continue
+ }
+
+ if !hostname.contains("://") {
+ hostname = "http://\(hostname)"
+ }
+
+ if hostname.starts(with: "http") {
+ return getIconUrl(hostname)
+ }
+ }
+
+ return nil
+ }
+
+ func getIconUrl(_ uriString:String?) -> String? {
+ guard let uriString = uriString else {
+ return nil
+ }
+
+ let hostname = URL.createFullUri(from: uriString)?.host
+ return hostname == nil ? "\(EnvironmentService.shared.iconsUrl)/icon.png" : "\(EnvironmentService.shared.iconsUrl)/\(hostname!)/icon.png"
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/KeychainHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/KeychainHelper.swift
new file mode 100644
index 000000000..e23a787a9
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/KeychainHelper.swift
@@ -0,0 +1,94 @@
+import Foundation
+
+final class KeychainHelper {
+
+ static let standard = KeychainHelper()
+ let genericService = "com.8bit.bitwarden.watch.kc"
+
+ private init() {}
+
+ func read(_ key: String, _ type: T.Type) -> T? where T : Codable {
+ guard let data = read(key) else {
+ return nil
+ }
+
+ do {
+ let item = try JSONDecoder().decode(type, from: data)
+ return item
+ } catch {
+ assertionFailure("Fail to decode item for keychain: \(error)")
+ return nil
+ }
+ }
+
+ func save(_ item: T, key: String) where T : Codable {
+
+ do {
+ let data = try JSONEncoder().encode(item)
+ save(data, key)
+
+ } catch {
+ assertionFailure("Fail to encode item for keychain: \(error)")
+ }
+ }
+
+ // MARK: NON-GENERIC FUNC
+ func read(_ key: String) -> Data? {
+ let query = [
+ kSecAttrService: genericService,
+ kSecAttrAccount: key,
+ kSecClass: kSecClassGenericPassword,
+ kSecReturnData: true
+ ] as CFDictionary
+
+ var result: AnyObject?
+ SecItemCopyMatching(query, &result)
+
+ return (result as? Data)
+ }
+
+ func save(_ data: Data, _ key: String) {
+ if let _ = read(key) {
+ delete(key)
+ }
+
+ let query = [
+ kSecValueData: data,
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: genericService,
+ kSecAttrAccount: key,
+ kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
+ ] as CFDictionary
+
+ let status = SecItemAdd(query, nil)
+
+ if status == errSecDuplicateItem {
+ // Item already exist, thus update it.
+ let query = [
+ kSecAttrService: genericService,
+ kSecAttrAccount: key,
+ kSecClass: kSecClassGenericPassword,
+ ] as CFDictionary
+
+ let attributesToUpdate = [kSecValueData: data] as CFDictionary
+
+ SecItemUpdate(query, attributesToUpdate)
+ }
+
+
+ if status != errSecSuccess {
+ Log.e("Error: \(status)")
+ }
+ }
+
+ func delete(_ key: String) {
+
+ let query = [
+ kSecAttrService: genericService,
+ kSecAttrAccount: key,
+ kSecClass: kSecClassGenericPassword,
+ ] as CFDictionary
+
+ SecItemDelete(query)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/LoggerHelper.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/LoggerHelper.swift
new file mode 100644
index 000000000..bcbb8abe4
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Helpers/LoggerHelper.swift
@@ -0,0 +1,54 @@
+import Foundation
+
+/// Wraps Swift.print() within DEBUG
+///
+/// - Note: *print()* might cause [security vulnerabilities](https://codifiedsecurity.com/mobile-app-security-testing-checklist-ios/)
+///
+/// - Parameter object: The object which is to be logged
+///
+func print(_ object: Any) {
+ #if DEBUG
+ Swift.print(object)
+ #endif
+}
+
+class Log{
+
+ static let shared = Log()
+
+ private init() {}
+
+ private static var isLoggingEnabled: Bool {
+ #if DEBUG
+ return true
+ #else
+ return false
+ #endif
+ }
+
+ static var dateFormat = "yyyy-MM-dd hh:mm:ssSSS"
+ static var dateFormatter: DateFormatter {
+ let formatter = DateFormatter()
+ formatter.dateFormat = dateFormat
+ formatter.locale = Locale.current
+ formatter.timeZone = TimeZone.current
+ return formatter
+ }
+
+ class func e( _ object: Any, filename: String = #file, line: Int = #line, column: Int = #column, funcName: String = #function) {
+ if isLoggingEnabled {
+ print("\(Date().toString()) Error [\(sourceFileName(filePath: filename))]:\(line) \(column) \(funcName) -> \(object)")
+ }
+ }
+
+ private class func sourceFileName(filePath: String) -> String {
+ let components = filePath.components(separatedBy: "/")
+ return components.isEmpty ? "" : components.last!
+ }
+}
+
+internal extension Date {
+ func toString() -> String {
+ return Log.dateFormatter.string(from: self as Date)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Info.plist b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Info.plist
new file mode 100644
index 000000000..17ed31521
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Info.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionAttributes
+
+ WKAppBundleIdentifier
+ com.8bit.bitwarden.watchkitapp
+
+ NSExtensionPointIdentifier
+ com.apple.watchkit
+
+
+
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings
new file mode 100644
index 000000000..8def799f6
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings
@@ -0,0 +1,9 @@
+"ThereAreNoItemsToList"="There are no items to list";
+"ToViewVerificationCodesUpgradeToPremium"="To view verification codes, upgrade to premium";
+"Add2FactorAutenticationToAnItemToViewVerificationCodes"="Add 2 factor authentication to an item to view the verification codes";
+"LogInToBitwardenOnYourIPhoneToViewVerificationCodes"="Log in to Bitwarden on your iPhone to view verification codes";
+"SyncingItemsContainingVerificationCodes"="Syncing items containing verification codes";
+"UnlockBitwardenOnYourIPhoneToViewVerificationCodes"="Unlock Bitwarden on your iPhone to view verification codes";
+"SetUpBitwardenToViewItemsContainingVerificationCodes"="Set up Bitwarden to view items containing verification codes";
+"Search"="Search";
+"NoItemsFound"="No items found";
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Cipher.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Cipher.swift
new file mode 100644
index 000000000..24232f2a0
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Cipher.swift
@@ -0,0 +1,36 @@
+import Foundation
+import CoreData
+
+struct Cipher:Identifiable,Codable{
+ var id:String
+ var name:String?
+ var userId:String?
+ var login:Login
+}
+
+struct Login:Codable{
+ var username:String?
+ var totp:String?
+ var uris:[LoginUri]?
+}
+
+struct LoginUri:Codable{
+ var uri:String?
+}
+
+extension Cipher{
+ func toCipherEntity(moContext: NSManagedObjectContext) -> CipherEntity{
+ let entity = CipherEntity(context: moContext)
+ entity.id = id
+ entity.name = name
+ entity.userId = userId ?? "unknown"
+ entity.username = login.username
+ entity.totp = login.totp
+
+ if let uris = login.uris, let encodedData = try? JSONEncoder().encode(uris) {
+ entity.loginUris = String(data: encodedData, encoding: .utf8)
+ }
+
+ return entity
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Mocks/CipherMock.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Mocks/CipherMock.swift
new file mode 100644
index 000000000..543e6a99e
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/Mocks/CipherMock.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+struct CipherMock {
+ static let ciphers:[Cipher] = [
+ Cipher(id: "0", name: "1933", userId: "123123", login: Login(username: "thisisatest@testing.com", totp: "otpauth://account?period=10&secret=LLLLLLLLLLLLLLLL", uris: cipherLoginUris)),
+ Cipher(id: "1", name: "GitHub", userId: "123123", login: Login(username: "thisisatest@testing.com", totp: "LLLLLLLLLLLLLLLL", uris: cipherLoginUris)),
+ Cipher(id: "2", name: "No user", userId: "123123", login: Login(username: nil, totp: "otpauth://account?period=10&digits=8&algorithm=sha256&secret=LLLLLLLLLLLLLLLL", uris: cipherLoginUris)),
+ Cipher(id: "3", name: "Site 2", userId: "123123", login: Login(username: "longtestemail000000@fastmailasdfasdf.com", totp: "otpauth://account?period=10&digits=7&algorithm=sha512&secret=LLLLLLLLLLLLLLLL", uris: cipherLoginUris)),
+ Cipher(id: "4", name: "Really long name for a site that is used for a totp", userId: "123123", login: Login(username: "user3", totp: "steam://LLLLLLLLLLLLLLLL", uris: cipherLoginUris)),
+ Cipher(id: "5", name: "Short", userId: "123123", login: Login(username: "u", totp: "steam://LLLLLLLLLLLLLLLL", uris: cipherLoginUris))
+ ]
+
+ static let cipherLoginUris:[LoginUri] = [
+ LoginUri(uri: "github.com"),
+ LoginUri(uri: "example2.com")
+ ]
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/User.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/User.swift
new file mode 100644
index 000000000..7320861e8
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/User.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+struct User : Codable {
+ var id: String
+ var email: String?
+ var name: String?
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/VaultTimeoutAction.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/VaultTimeoutAction.swift
new file mode 100644
index 000000000..6c6fc1656
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/VaultTimeoutAction.swift
@@ -0,0 +1,6 @@
+//import Foundation
+//
+//enum VaultTimeoutAction : Int, Codable {
+// case lock = 0
+// case logout = 1
+//}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/WatchDTO.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/WatchDTO.swift
new file mode 100644
index 000000000..429a54465
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Models/WatchDTO.swift
@@ -0,0 +1,26 @@
+import Foundation
+
+struct WatchDTO : Codable{
+ var state: BWState
+ var ciphers: [Cipher]?
+ var userData: User?
+ var environmentData: EnvironmentUrlDataDto?
+// var settingsData: SettingsDataDto?
+
+ init(state: BWState) {
+ self.state = state
+ self.ciphers = nil
+ self.userData = nil
+ self.environmentData = nil
+ }
+}
+
+struct EnvironmentUrlDataDto : Codable {
+ var base: String?
+ var icons: String?
+}
+
+//struct SettingsDataDto : Codable {
+// var vaultTimeoutInMinutes: Int?
+// var vaultTimeoutAction: VaultTimeoutAction
+//}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationController.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationController.swift
new file mode 100644
index 000000000..73fceb36b
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationController.swift
@@ -0,0 +1,26 @@
+import WatchKit
+import SwiftUI
+import UserNotifications
+
+class NotificationController: WKUserNotificationHostingController {
+
+ override var body: NotificationView {
+ return NotificationView()
+ }
+
+ override func willActivate() {
+ // This method is called when watch view controller is about to be visible to user
+ super.willActivate()
+ }
+
+ override func didDeactivate() {
+ // This method is called when watch view controller is no longer visible
+ super.didDeactivate()
+ }
+
+ override func didReceive(_ notification: UNNotification) {
+ // This method is called when a notification needs to be presented.
+ // Implement it if you use a dynamic notification interface.
+ // Populate your dynamic notification interface as quickly as possible.
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationView.swift
new file mode 100644
index 000000000..63211dd15
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/NotificationView.swift
@@ -0,0 +1,13 @@
+import SwiftUI
+
+struct NotificationView: View {
+ var body: some View {
+ Text("Hello, World!")
+ }
+}
+
+struct NotificationView_Previews: PreviewProvider {
+ static var previews: some View {
+ NotificationView()
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/PushNotificationPayload.apns b/src/watchOS/bitwarden/bitwarden WatchKit Extension/PushNotificationPayload.apns
new file mode 100644
index 000000000..c18b00ad9
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/PushNotificationPayload.apns
@@ -0,0 +1,20 @@
+{
+ "aps": {
+ "alert": {
+ "body": "Test message",
+ "title": "Optional title",
+ "subtitle": "Optional subtitle"
+ },
+ "category": "myCategory",
+ "thread-id": "5280"
+ },
+
+ "WatchKit Simulator Actions": [
+ {
+ "title": "First Button",
+ "identifier": "firstButtonAction"
+ }
+ ],
+
+ "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App."
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CipherService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CipherService.swift
new file mode 100644
index 000000000..6328ae40c
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CipherService.swift
@@ -0,0 +1,70 @@
+import Foundation
+import CoreData
+
+protocol CipherServiceProtocol {
+ func getCipher(_ id: String) -> Cipher?
+ func fetchCiphers(_ withUserId: String?) -> [Cipher]
+ func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void)
+ func deleteAll(_ withUserId: String?, completionHandler: @escaping () -> Void)
+}
+
+class CipherService {
+ static let shared: CipherServiceProtocol = CipherService()
+
+ var dbHelper: CoreDataHelper = CoreDataHelper.shared
+
+ private init() { }
+
+ func getCipher(_ id: String) -> Cipher? {
+ let predicate = NSPredicate(
+ format: "id = %@",
+ id as CVarArg)
+ let result = dbHelper.fetchFirst(CipherEntity.self, predicate: predicate)
+ switch result {
+ case .success(let cipherEntity):
+ return cipherEntity?.toCipher()
+ case .failure(_):
+ return nil
+ }
+ }
+}
+
+// MARK: - CipherServiceProtocol
+extension CipherService: CipherServiceProtocol {
+ func fetchCiphers(_ withUserId: String?) -> [Cipher] {
+ let result: Result<[CipherEntity], Error> = dbHelper.fetch(CipherEntity.self, "CipherEntity", predicate: withUserId == nil ? nil : NSPredicate(format: "userId = %@", withUserId!))
+ switch result {
+ case .success(let success):
+ return success.map { entity in entity.toCipher() }
+ case .failure(let error):
+ fatalError(error.localizedDescription)
+ }
+ }
+
+ func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void){
+ dbHelper.insertBatch("CipherEntity", items: ciphers) { item, context in
+ guard let cipher = item as! Cipher? else { return [:] }
+ let c = cipher.toCipherEntity(moContext: context)
+ guard let data = try? JSONEncoder().encode(c) else
+ {
+ Log.e("Error converting to data")
+ return [:]
+ }
+
+ guard let cipherDict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any ] else
+ {
+ Log.e("Error converting json data to dict")
+ return [:]
+ }
+ return cipherDict
+
+ } completionHandler: {
+ completionHandler()
+ }
+ }
+
+ func deleteAll(_ withUserId: String? = nil, completionHandler: @escaping () -> Void) {
+ let predicate = withUserId == nil ? nil : NSPredicate(format: "userId = %@", withUserId!)
+ dbHelper.deleteAll("CipherEntity", predicate: predicate, completionHandler: completionHandler)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoFunctionService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoFunctionService.swift
new file mode 100644
index 000000000..eb2efa6bc
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoFunctionService.swift
@@ -0,0 +1,25 @@
+import Foundation
+import CryptoKit
+
+class CryptoFunctionService{
+ static let shared: CryptoFunctionService = CryptoFunctionService()
+
+ enum CryptoHashAlgorithm {
+ case Sha1, Sha256, Sha512
+ }
+
+ enum CryptoError: Error {
+ case AlgorithmNotImplemented
+ }
+
+ func hmac(_ data: Data, _ key: SymmetricKey, algorithm alg: CryptoHashAlgorithm) -> Data {
+ switch alg {
+ case .Sha1:
+ return Data(HMAC.authenticationCode(for: data, using: key))
+ case .Sha256:
+ return Data(HMAC.authenticationCode(for: data, using: key))
+ case .Sha512:
+ return Data(HMAC.authenticationCode(for: data, using: key))
+ }
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoService.swift
new file mode 100644
index 000000000..1a5f6258d
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/CryptoService.swift
@@ -0,0 +1,97 @@
+import Foundation
+import CryptoKit
+
+public class CryptoService{
+ static let ENCRYPTION_KEY: String = "encryptionKey"
+
+ private(set) var key: SymmetricKey? = nil
+
+ init(){
+ key = loadKey()
+ }
+
+ func encrypt(_ plainText: String?) -> Data? {
+ guard let plainText = plainText, let key = key else {
+ return nil
+ }
+
+ let nonce = randomData(lengthInBytes: 12)
+
+ let plainData = plainText.data(using: .utf8)
+ let sealedData = try! AES.GCM.seal(plainData!, using: key, nonce: AES.GCM.Nonce(data:nonce))
+ return sealedData.combined
+ }
+
+ func decrypt(_ combinedEncryptedData: Data, _ type: T.Type) -> T? {
+ guard let key = key else {
+ return nil
+ }
+
+ let sealedBox = try! AES.GCM.SealedBox(combined: combinedEncryptedData)
+ let decryptedData = try! AES.GCM.open(sealedBox, using: key)
+
+ do {
+ let item = try JSONDecoder().decode(type, from: decryptedData)
+ return item
+ } catch {
+ assertionFailure("Fail to decode item for keychain: \(error)")
+ return nil
+ }
+ }
+
+ func decrypt(_ combinedEncryptedData: Data) -> Data? {
+ guard let key = key else {
+ return nil
+ }
+
+ let sealedBox = try! AES.GCM.SealedBox(combined: combinedEncryptedData)
+ let decryptedData = try! AES.GCM.open(sealedBox, using: key)
+ return decryptedData
+ }
+
+ func loadKey() -> SymmetricKey{
+ if let encKey = KeychainHelper.standard.read(CryptoService.ENCRYPTION_KEY) {
+ return SymmetricKey(data: encKey)
+ }
+
+ // First time so we need to generate the key
+ let newKey = SymmetricKey(size: .bits256)
+ let keyData = newKey.withUnsafeBytes({ body in
+ return Data(Array(body))
+ })
+ KeychainHelper.standard.save(keyData, CryptoService.ENCRYPTION_KEY)
+ return newKey
+ }
+
+ func randomData(lengthInBytes: Int) -> Data {
+ var data = Data(count: lengthInBytes)
+ _ = data.withUnsafeMutableBytes {
+ SecRandomCopyBytes(kSecRandomDefault, lengthInBytes, $0.baseAddress!)
+ }
+ return data
+ }
+}
+
+public extension Data {
+ init?(hexString: String) {
+ let len = hexString.count / 2
+ var data = Data(capacity: len)
+ var i = hexString.startIndex
+ for _ in 0.. [Cipher] {
+ return ciphers
+ }
+
+ func deleteAll(_ withUserId: String?, completionHandler: @escaping () -> Void) {
+ completionHandler()
+ }
+
+ func getCipher(_ id: String) -> Cipher? {
+ return CipherMock.ciphers.first { ci in
+ ci.id == id
+ }
+ }
+
+ func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void) {
+ }
+
+ private var ciphers = [Cipher]()
+
+ init() {
+ ciphers = CipherMock.ciphers
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/StateService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/StateService.swift
new file mode 100644
index 000000000..97f008520
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/StateService.swift
@@ -0,0 +1,73 @@
+import Foundation
+
+class StateService {
+ static let shared: StateService = StateService()
+
+ let CURRENT_STATE_KEY = "current_state_key"
+ let CURRENT_USER_KEY = "current_user_key"
+// let TIMEOUT_MINUTES_KEY = "timeout_minutes_key"
+// let TIMEOUT_ACTION_KEY = "timeout_action_key"
+
+ private init(){}
+
+ var currentState:BWState {
+ get {
+ guard let stateData = KeychainHelper.standard.read(CURRENT_STATE_KEY),
+ let strData = String(data: stateData, encoding: .utf8),
+ let intData = Int(strData),
+ let state = BWState(rawValue: intData) else {
+ return BWState.needSetup
+ }
+
+ return state
+ }
+ set(newState) {
+ let stateVal = String(newState.rawValue)
+ KeychainHelper.standard.save(stateVal.data(using: .utf8)!, CURRENT_STATE_KEY)
+ }
+ }
+
+ func getUser() -> User? {
+ return KeychainHelper.standard.read(CURRENT_USER_KEY, User.self)
+ }
+
+ func setUser(user: User?) {
+ guard let user = user else {
+ KeychainHelper.standard.delete(CURRENT_USER_KEY)
+ return
+ }
+
+ KeychainHelper.standard.save(user, key: CURRENT_USER_KEY)
+ }
+
+// var vaultTimeoutInMinutes: Int? {
+// guard let timeoutData = KeychainHelper.standard.read(TIMEOUT_MINUTES_KEY),
+// let strData = String(data: timeoutData, encoding: .utf8),
+// let intVal = Int(strData) else {
+// return nil
+// }
+//
+// return intVal
+// }
+
+// var vaultTimeoutAction: VaultTimeoutAction {
+// guard let timeoutActionData = KeychainHelper.standard.read(TIMEOUT_ACTION_KEY),
+// let strData = String(data: timeoutActionData, encoding: .utf8),
+// let intData = Int(strData),
+// let timeoutAction = VaultTimeoutAction(rawValue: intData) else {
+// return .lock
+// }
+//
+// return timeoutAction
+// }
+
+// func setVaultTimeout(_ timeoutInMinutes: Int?, _ action: VaultTimeoutAction) {
+// guard let timeoutInMinutes = timeoutInMinutes else {
+// KeychainHelper.standard.delete(TIMEOUT_MINUTES_KEY)
+// return
+// }
+//
+// KeychainHelper.standard.save(String(timeoutInMinutes).data(using: .utf8)!, TIMEOUT_MINUTES_KEY)
+// KeychainHelper.standard.save(String(action.rawValue).data(using: .utf8)!, TIMEOUT_ACTION_KEY)
+// }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/TotpService.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/TotpService.swift
new file mode 100644
index 000000000..f851bc365
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Services/TotpService.swift
@@ -0,0 +1,133 @@
+import Foundation
+import CryptoKit
+
+class TotpService{
+ struct CodeConfig{
+ let period:Int
+ let digits:Int
+ let algorithm:CryptoFunctionService.CryptoHashAlgorithm
+ let keyB32:String?
+ }
+
+ static let shared: TotpService = TotpService()
+
+ static let STEAM_CHARS = "23456789BCDFGHJKMNPQRTVWXY";
+ static let TOTP_DEFAULT_TIMER: Int = 30
+
+ func GetCodeAsync(key: String?) throws -> String?{
+ guard let key = key, !key.isEmpty else {
+ return nil
+ }
+
+ var config = CodeConfig(period: TotpService.TOTP_DEFAULT_TIMER, digits: 6, algorithm: CryptoFunctionService.CryptoHashAlgorithm.Sha1, keyB32: key)
+
+ let isOtpAuth = key.lowercased().starts(with: "otpauth://");
+ let isSteamAuth = key.lowercased().starts(with: "steam://");
+
+ if isOtpAuth {
+ guard let keyUrl = URLComponents(string: key) else {
+ return nil
+ }
+
+ if let queryItems = keyUrl.queryItems {
+ config = getCodeConfigFrom(queryItems, config)
+ }
+ } else if isSteamAuth {
+ let keyIndexOffset = key.index(key.startIndex, offsetBy: 8)
+
+ config = CodeConfig(period: config.period, digits: 5, algorithm: config.algorithm, keyB32: String(key.suffix(from: keyIndexOffset)))
+ }
+
+ guard let keyB32 = config.keyB32 else {
+ return nil
+ }
+
+ let keyBytes = try Base32.fromBase32(keyB32)
+ if keyBytes.count == 0 {
+ return nil
+ }
+
+ let counter = UInt64(Date().timeIntervalSince1970 / TimeInterval(config.period)).bigEndian
+ let hash = CryptoFunctionService.shared.hmac(counter.data, SymmetricKey(data:Data(keyBytes)), algorithm: config.algorithm)
+ if hash.count == 0
+ {
+ return nil;
+ }
+
+ let offset = Int(hash[hash.count - 1] & 0xf)
+ let binary = Int32(hash[offset] & 0x7f) << 24 | Int32(hash[offset + 1] & 0xff) << 16 | Int32(hash[offset + 2] & 0xff) << 8 | Int32(hash[offset + 3] & 0xff)
+
+ var otp = "";
+ if (isSteamAuth) {
+ var fullCode = Int(binary & 0x7fffffff)
+ for _ in 0.. CodeConfig {
+ var period:Int?
+ var digits:Int?
+ var algorithm:CryptoFunctionService.CryptoHashAlgorithm?
+ var keyB32:String?
+
+ for item in queryItems {
+ if item.name == "digits",
+ let digitsVal = item.value,
+ let digitParam = Int(digitsVal.trimmingCharacters(in: .whitespacesAndNewlines)) {
+ if (digitParam > 10){
+ digits = 10;
+ } else if (digitParam > 0){
+ digits = digitParam;
+ }
+ } else if item.name == "period",
+ let periodVal = item.value,
+ let periodParam = Int(periodVal.trimmingCharacters(in: .whitespacesAndNewlines)),
+ periodParam > 0 {
+ period = periodParam
+ } else if item.name == "secret", let secretVal = item.value {
+ keyB32 = secretVal
+ } else if item.name == "algorithm", let algorithmVal = item.value {
+ if algorithmVal.lowercased() == "sha256" {
+ algorithm = CryptoFunctionService.CryptoHashAlgorithm.Sha256;
+ }
+ else if algorithmVal.lowercased() == "sha512" {
+ algorithm = CryptoFunctionService.CryptoHashAlgorithm.Sha512;
+ }
+ }
+ }
+
+ return CodeConfig(period: period ?? currentConfig.period,
+ digits: digits ?? currentConfig.digits,
+ algorithm: algorithm ?? currentConfig.algorithm,
+ keyB32: keyB32 ?? currentConfig.keyB32)
+ }
+
+ func getPeriodFrom(_ key: String) -> Int {
+ guard key.lowercased().starts(with: "otpauth://"),
+ let keyUrl = URLComponents(string: key),
+ let queryItems = keyUrl.queryItems else {
+ return TotpService.TOTP_DEFAULT_TIMER
+ }
+
+ let periodQueryItem = queryItems.first { qi in qi.name == "period" }
+ guard let periodValue = periodQueryItem?.value,
+ let periodInt = Int(periodValue.trimmingCharacters(in: .whitespacesAndNewlines)),
+ periodInt > 0 else {
+ return TotpService.TOTP_DEFAULT_TIMER
+ }
+ return periodInt
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/BWState.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/BWState.swift
new file mode 100644
index 000000000..bc4c22ed1
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/BWState.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+enum BWState : Int, Codable {
+ case valid = 0
+ case needLogin = 1
+ case needPremium = 2
+ case needSetup = 3
+ case need2FAItem = 4
+ case syncing = 5
+ // case needUnlock = 6
+
+ var isDestructive: Bool {
+ return self == .needSetup || self == .needLogin || self == .needPremium || self == .need2FAItem
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/Base32.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/Base32.swift
new file mode 100644
index 000000000..ca58bb278
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/Base32.swift
@@ -0,0 +1,58 @@
+import Foundation
+
+enum Base32Error: Error {
+ case invalidFormat
+}
+
+final class Base32 {
+ private static let BASE_32_CHARS:String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+
+ static func fromBase32(_ rawInput: String) throws -> [UInt8] {
+ var input = rawInput.uppercased()
+ var cleanedInput = "";
+ for c in input {
+ if (BASE_32_CHARS.firstIndex(of: c) != nil){
+ cleanedInput.append(c);
+ }
+ }
+
+ input = cleanedInput;
+ if input.count == 0 {
+ return [UInt8]()
+ }
+
+ var output = [UInt8](repeating: 0, count: input.count * 5 / 8) // new byte[input.Length * 5 / 8];
+ var bitIndex = 0;
+ var inputIndex = 0;
+ var outputBits = 0;
+ var outputIndex = 0;
+
+ while outputIndex < output.count {
+ guard let byteIndex = BASE_32_CHARS.firstIndex(of: input[input.index(input.startIndex, offsetBy:inputIndex)]) else {
+ throw Base32Error.invalidFormat
+ }
+
+ let byteIndexInt = BASE_32_CHARS.distance(from: BASE_32_CHARS.startIndex, to: byteIndex)
+
+ let bits = min(5 - bitIndex, 8 - outputBits);
+ output[outputIndex] <<= bits;
+ output[outputIndex] |= (UInt8)(byteIndexInt >> (5 - (bitIndex + bits)));
+
+ bitIndex += bits;
+ if (bitIndex >= 5)
+ {
+ inputIndex += 1;
+ bitIndex = 0;
+ }
+
+ outputBits += bits;
+ if (outputBits >= 8)
+ {
+ outputIndex += 1;
+ outputBits = 0;
+ }
+ }
+
+ return output;
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ColorUtils.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ColorUtils.swift
new file mode 100644
index 000000000..58242e640
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ColorUtils.swift
@@ -0,0 +1,60 @@
+import SwiftUI
+
+extension Color {
+ static let ui = Color.UI()
+
+ struct UI {
+ let primary = Color(hex: "#175DDC")
+ let itemBackground = Color("ItemBackground")
+ let darkTextMuted = Color("DarkTextMuted")
+ }
+
+ init?(hex: String) {
+ var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
+ hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
+
+ var rgb: UInt64 = 0
+
+ var r: CGFloat = 0.0
+ var g: CGFloat = 0.0
+ var b: CGFloat = 0.0
+ var a: CGFloat = 1.0
+
+ guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else{
+ return nil
+ }
+
+ switch hexSanitized.count {
+ case 3:
+ r = CGFloat((rgb >> 8) * 17) / 255.0
+ g = CGFloat((rgb >> 4 & 0xF) * 17) / 255.0
+ b = CGFloat((rgb & 0xF) * 17) / 255.0
+ case 6:
+ r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
+ g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
+ b = CGFloat(rgb & 0x0000FF) / 255.0
+ case 8:
+ r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
+ g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
+ b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
+ a = CGFloat(rgb & 0x000000FF) / 255.0
+ default:
+ return nil
+ }
+
+ self.init(red: r, green: g, blue: b, opacity: a)
+ }
+
+ var components: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) {
+ var r: CGFloat = 0
+ var g: CGFloat = 0
+ var b: CGFloat = 0
+ var o: CGFloat = 0
+
+ guard UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &o) else {
+ return (0, 0, 0, 0)
+ }
+
+ return (r, g, b, o)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/DateExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/DateExtensions.swift
new file mode 100644
index 000000000..8ba7d97e6
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/DateExtensions.swift
@@ -0,0 +1,8 @@
+import Foundation
+
+extension Date{
+ var epocUtcNowInMs: Int {
+ return Int(self.timeIntervalSince1970 * 1_000)
+ }
+
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/EmptyStateViewModifier.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/EmptyStateViewModifier.swift
new file mode 100644
index 000000000..eaed3c430
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/EmptyStateViewModifier.swift
@@ -0,0 +1,23 @@
+import Foundation
+import SwiftUI
+
+struct EmptyStateViewModifier: ViewModifier where EmptyContent: View {
+ var isEmpty: Bool
+ let emptyContent: () -> EmptyContent
+
+ func body(content: Content) -> some View {
+ if isEmpty {
+ emptyContent()
+ }
+ else {
+ content
+ }
+ }
+}
+
+extension View {
+ func emptyState(_ isEmpty: Bool,
+ emptyContent: @escaping () -> EmptyContent) -> some View where EmptyContent: View {
+ modifier(EmptyStateViewModifier(isEmpty: isEmpty, emptyContent: emptyContent))
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ErrorExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ErrorExtensions.swift
new file mode 100644
index 000000000..6320fe5ca
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ErrorExtensions.swift
@@ -0,0 +1,69 @@
+import Foundation
+
+let theOperationCouldNotBeCompleted = "The operation could not be completed."
+
+extension Error {
+ /// - Returns: A fully qualified representation of this error.
+ public var legibleDescription: String {
+ switch errorType {
+ case .swiftError(.enum?), .swiftLocalizedError(_, .enum?):
+ return "\(type(of: self)).\(self)"
+ case .swiftError(.class?), .swiftLocalizedError(_, .class?):
+ return "\(type(of: self))"
+ case .swiftError, .swiftLocalizedError:
+ return String(describing: self)
+ case let .nsError(nsError, domain, code):
+ if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError {
+ return "\(domain)(\(code), \(underlyingError.domain)(\(underlyingError.code)))"
+ } else {
+ return "\(domain)(\(code))"
+ }
+ }
+ }
+
+ /// - Returns: A fully qualified, user-visible representation of this error.
+ public var legibleLocalizedDescription: String {
+ switch errorType {
+ case .swiftError:
+ return "\(theOperationCouldNotBeCompleted) (\(legibleDescription))"
+ case .swiftLocalizedError(let msg, _):
+ return msg
+ case .nsError(_, "kCLErrorDomain", 0):
+ return "The location could not be determined."
+ // ^^ Apple don’t provide a localized description for this
+ case let .nsError(nsError, domain, code):
+ if !localizedDescription.hasPrefix(theOperationCouldNotBeCompleted) {
+ return localizedDescription
+ //FIXME ^^ for non-EN
+ } else if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? Error {
+ return underlyingError.legibleLocalizedDescription
+ } else {
+ // usually better than the localizedDescription, but not pretty
+ return "\(theOperationCouldNotBeCompleted) (\(domain).\(code))"
+ }
+ }
+ }
+
+ private var errorType: ErrorType {
+ let foo: Any = self
+ let nativeClassNames = ["_SwiftNativeNSError", "__SwiftNativeNSError"]
+ let selfClassName = String(cString: object_getClassName(self))
+ let isNSError = !nativeClassNames.contains(selfClassName) && foo is NSObject
+ // ^^ ∵ otherwise implicit bridging implicitly casts as for other tests
+
+ if isNSError {
+ let nserr = self as NSError
+ return .nsError(nserr, domain: nserr.domain, code: nserr.code)
+ } else if let err = self as? LocalizedError, let msg = err.errorDescription {
+ return .swiftLocalizedError(msg, Mirror(reflecting: self).displayStyle)
+ } else {
+ return .swiftError(Mirror(reflecting: self).displayStyle)
+ }
+ }
+}
+
+private enum ErrorType {
+ case nsError(NSError, domain: String, code: Int)
+ case swiftLocalizedError(String, Mirror.DisplayStyle?)
+ case swiftError(Mirror.DisplayStyle?)
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/JsonDecoderExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/JsonDecoderExtensions.swift
new file mode 100644
index 000000000..c40a0303b
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/JsonDecoderExtensions.swift
@@ -0,0 +1,40 @@
+import Foundation
+
+extension JSONDecoder.KeyDecodingStrategy {
+ static var upperToLowerCamelCase: JSONDecoder.KeyDecodingStrategy {
+ return .custom { codingKeys in
+
+ var key = AnyCodingKey(codingKeys.last!)
+
+ if let firstChar = key.stringValue.first {
+ key.stringValue.replaceSubrange(
+ ...key.stringValue.startIndex, with: String(firstChar).lowercased()
+ )
+ }
+ return key
+ }
+ }
+}
+
+struct AnyCodingKey : CodingKey {
+ var stringValue: String
+ var intValue: Int?
+
+ init(_ base: CodingKey) {
+ self.init(stringValue: base.stringValue, intValue: base.intValue)
+ }
+
+ init(stringValue: String) {
+ self.stringValue = stringValue
+ }
+
+ init(intValue: Int) {
+ self.stringValue = "\(intValue)"
+ self.intValue = intValue
+ }
+
+ init(stringValue: String, intValue: Int?) {
+ self.stringValue = stringValue
+ self.intValue = intValue
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/StringExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/StringExtensions.swift
new file mode 100644
index 000000000..b5441d771
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/StringExtensions.swift
@@ -0,0 +1,28 @@
+import Foundation
+
+extension String {
+ static func isEmpty(_ s:String?) -> Bool {
+ guard let s = s else {
+ return true
+ }
+
+ return s.isEmpty
+ }
+
+ static func isEmptyOrWhitespace(_ s: String?) -> Bool {
+ guard let s = s else {
+ return true
+ }
+
+ return s.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+
+ func leftPadding(toLength: Int, withPad character: Character) -> String {
+ let currentLength = self.count
+ if currentLength < toLength {
+ return String(repeatElement(character, count: toLength - currentLength)) + self
+ } else {
+ return String(self.suffix(toLength))
+ }
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/UInt64Extensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/UInt64Extensions.swift
new file mode 100644
index 000000000..83f5b7c84
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/UInt64Extensions.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+extension UInt64 {
+ var data: Data {
+ var int64 = self
+ let int64Data = Data(bytes: &int64, count: MemoryLayout.size(ofValue: self))
+ return int64Data
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/URLExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/URLExtensions.swift
new file mode 100644
index 000000000..c8221e80a
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/URLExtensions.swift
@@ -0,0 +1,28 @@
+import Foundation
+
+extension URL {
+ static func createFullUri(from uriString:String?) -> URL? {
+ guard let uriString = uriString else {
+ return nil
+ }
+
+ let hasHttpScheme = uriString.starts(with: "http://") || uriString.starts(with: "https://")
+ if !hasHttpScheme && !uriString.contains("://") && uriString.contains(".") {
+ if let uri = URL(string: "http://\(uriString)") {
+ return uri
+ }
+ }
+ guard let uri2 = URL(string: uriString) else {
+ return nil
+ }
+
+ return uri2
+ }
+
+ var host:String? {
+ if let components = URLComponents(url: self, resolvingAgainstBaseURL: false){
+ return components.host
+ }
+ return nil
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ViewExtensions.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ViewExtensions.swift
new file mode 100644
index 000000000..d008fa74d
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Utilities/ViewExtensions.swift
@@ -0,0 +1,15 @@
+import Foundation
+import SwiftUI
+
+extension View {
+ func placeholder(
+ when shouldShow: Bool,
+ alignment: Alignment = .leading,
+ @ViewBuilder placeholder: () -> Content) -> some View {
+
+ ZStack(alignment: alignment) {
+ placeholder().opacity(shouldShow ? 1 : 0)
+ self
+ }
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/BWStateViewModel.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/BWStateViewModel.swift
new file mode 100644
index 000000000..ac841086e
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/BWStateViewModel.swift
@@ -0,0 +1,26 @@
+import Foundation
+
+class BWStateViewModel : ObservableObject{
+ @Published var text:String
+ @Published var isLoading:Bool = false
+
+ init(_ state: BWState){
+ switch state {
+ case .needLogin:
+ text = "LogInToBitwardenOnYourIPhoneToViewVerificationCodes"
+// case .needUnlock:
+// text = "UnlockBitwardenOnYourIPhoneToViewVerificationCodes"
+ case .needPremium:
+ text = "ToViewVerificationCodesUpgradeToPremium"
+ case .needSetup:
+ text = "SetUpBitwardenToViewItemsContainingVerificationCodes"
+ case .syncing:
+ text = "SyncingItemsContainingVerificationCodes"
+ isLoading = true
+ case .need2FAItem:
+ text = "Add2FactorAutenticationToAnItemToViewVerificationCodes"
+ default:
+ text = ""
+ }
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherDetailsViewModel.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherDetailsViewModel.swift
new file mode 100644
index 000000000..bfed0a30b
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/ViewModels/CipherDetailsViewModel.swift
@@ -0,0 +1,63 @@
+import Foundation
+import SwiftUI
+
+class CipherDetailsViewModel: ObservableObject{
+ @Published var cipher:Cipher
+
+ @Published var totpFormatted:String = ""
+ @Published var progress:Double = 1
+ @Published var counter:Int
+ @Published var iconImageUri:String?
+
+ var key: String
+ var period: Int
+ var timer: Timer? = nil
+
+ init(cipher: Cipher) {
+ self.cipher = cipher
+ self.key = cipher.login.totp!
+ self.period = TotpService.shared.getPeriodFrom(key)
+ self.counter = period
+ self.iconImageUri = IconImageHelper.shared.getLoginIconImage(cipher)
+ }
+
+ func startGeneration() {
+ self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] t in
+ guard let self = self else {
+ t.invalidate()
+ return
+ }
+
+ let epoc = Int64(Date().timeIntervalSince1970)
+ let mod = Int(epoc % Int64(self.period))
+ DispatchQueue.main.async {
+ self.counter = self.period - mod
+ self.progress = Double(self.counter) / Double(self.period)
+ }
+ if mod == 0 || self.totpFormatted == "" {
+ do {
+ var totpF = try TotpService.shared.GetCodeAsync(key: self.key) ?? ""
+ if totpF.count > 4 {
+ let halfIndex = totpF.index(totpF.startIndex, offsetBy: totpF.count / 2)
+ totpF = "\(totpF[totpF.startIndex.. Bool {
+ if searchTerm.isEmpty {
+ return true
+ }
+
+ if(cipher.name?.lowercased().contains(searchTerm.lowercased()) ?? false) {
+ return true
+ }
+
+ if (cipher.login.username?.lowercased().contains(searchTerm.lowercased()) ?? false) {
+ return true
+ }
+
+ return false
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/BWStateView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/BWStateView.swift
new file mode 100644
index 000000000..05436a17c
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/BWStateView.swift
@@ -0,0 +1,37 @@
+import SwiftUI
+
+struct BWStateView: View {
+ @ObservedObject var viewModel:BWStateViewModel
+
+ init(_ state: BWState) {
+ viewModel = BWStateViewModel(state)
+ }
+
+ var body: some View {
+ VStack(alignment: .center) {
+ Spacer()
+ Image("BitwardenImagetype")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: .infinity, height: 35)
+ .padding(.leading, 15)
+ .padding(.trailing, 15)
+ .padding(.top, 5)
+ Spacer()
+ Text(LocalizedStringKey(viewModel.text))
+ .font(.title3)
+ .fontWeight(.semibold)
+ .multilineTextAlignment(.center)
+ if viewModel.isLoading {
+ ProgressView()
+ .frame(width: 20, height: 20)
+ }
+ }
+ }
+}
+
+struct BWStateView_Previews: PreviewProvider {
+ static var previews: some View {
+ BWStateView(.needSetup)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherDetailsView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherDetailsView.swift
new file mode 100644
index 000000000..7ac031004
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherDetailsView.swift
@@ -0,0 +1,101 @@
+import SwiftUI
+
+struct CipherDetailsView: View {
+ @ObservedObject var cipherDetailsViewModel: CipherDetailsViewModel
+
+ let iconSize: CGSize = CGSize(width: 30, height: 30)
+
+ init(cipher: Cipher) {
+ self.cipherDetailsViewModel = CipherDetailsViewModel(cipher: cipher)
+ }
+
+ var body: some View {
+ VStack(alignment:.leading){
+ HStack{
+ if cipherDetailsViewModel.iconImageUri == nil {
+ iconPlaceholderImage
+ } else {
+ if #available(watchOSApplicationExtension 8.0, *) {
+ AsyncImage(url: URL(string: cipherDetailsViewModel.iconImageUri!)){ phase in
+ switch phase {
+ case .empty:
+ iconPlaceholderImage
+ case .success(let image):
+ image.resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(maxWidth: iconSize.width, maxHeight: iconSize.height)
+ case .failure:
+ iconPlaceholderImage
+ @unknown default:
+ EmptyView()
+ }
+ }
+ } else {
+ ImageView(withURL: cipherDetailsViewModel.iconImageUri!, maxWidth: iconSize.width, maxHeight: iconSize.height) {
+ iconPlaceholderImage
+ }
+ }
+ }
+ Text(cipherDetailsViewModel.cipher.name!)
+ .font(.title2)
+ .fontWeight(.bold)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .padding(.leading, 5)
+ }
+ if cipherDetailsViewModel.cipher.login.username != nil {
+ Text(cipherDetailsViewModel.cipher.login.username!)
+ .font(.title3)
+ .fontWeight(.bold)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ }
+ if cipherDetailsViewModel.totpFormatted == "" {
+ ProgressView()
+ } else {
+ HStack{
+ let transition = AnyTransition.asymmetric(insertion: .slide, removal: .scale).combined(with: .opacity)
+ Text(cipherDetailsViewModel.totpFormatted)
+ .font(.largeTitle)
+ .scaledToFit()
+ .minimumScaleFactor(0.01)
+ .lineLimit(1)
+ .id(cipherDetailsViewModel.totpFormatted)
+ .transition(transition)
+ .animation(.default.speed(0.7), value: cipherDetailsViewModel.totpFormatted)
+ Spacer()
+ ZStack{
+ CircularProgressView(progress: cipherDetailsViewModel.progress, strokeLineWidth: 3, strokeColor: Color.blue, endingStrokeColor: Color.red)
+ .frame(width: 40, height:40)
+ Text("\(cipherDetailsViewModel.counter)")
+ .font(.title3)
+ .fontWeight(.semibold)
+ }
+ }
+ .padding(.top, 20)
+ .padding(.leading, 5)
+ .padding(.trailing, 5)
+ }
+ }
+ .onAppear{
+ self.cipherDetailsViewModel.startGeneration()
+ }
+ .onDisappear{
+ self.cipherDetailsViewModel.stopGeneration()
+ }
+ }
+
+ var iconPlaceholderImage: some View{
+ Image("DefaultCipherIcon")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(maxWidth: iconSize.width, maxHeight: iconSize.height)
+
+ }
+}
+
+struct CipherDetailsView_Previews: PreviewProvider {
+ static var previews: some View {
+ CipherDetailsView(cipher: CipherMock.ciphers[0])
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherItemView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherItemView.swift
new file mode 100644
index 000000000..4747a064c
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherItemView.swift
@@ -0,0 +1,57 @@
+import SwiftUI
+
+struct CipherItemView: View {
+ let cipher:Cipher
+ let maxWidth:CGFloat
+
+ init(_ cipher:Cipher, _ maxWidth:CGFloat) {
+ self.cipher = cipher
+ self.maxWidth = maxWidth
+ }
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ if cipher.id == "-1" {
+ // Workaround: To display 0 results on search
+ // and the message to be localized
+ Text(LocalizedStringKey(cipher.name!))
+ .font(.title3)
+ .fontWeight(.bold)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ } else {
+ Text(cipher.name ?? "")
+ .font(.title3)
+ .fontWeight(.bold)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ if cipher.login.username != nil {
+ Text(cipher.login.username! )
+ .font(.body)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .foregroundColor(Color.ui.darkTextMuted)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 5)
+ .foregroundColor(Color.ui.itemBackground)
+ .frame(width: maxWidth,
+ alignment: .leading)
+ )
+ .frame(width: maxWidth,
+ alignment: .leading)
+ }
+}
+
+struct CipherItemView_Previews: PreviewProvider {
+ static var previews: some View {
+ CipherItemView(CipherMock.ciphers[0], .infinity)
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherListView.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherListView.swift
new file mode 100644
index 000000000..4c40568a0
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Views/CipherListView.swift
@@ -0,0 +1,134 @@
+import SwiftUI
+
+struct CipherListView: View {
+ @ObservedObject var viewModel = CipherListViewModel(CipherService.shared)
+
+ let AVATAR_ID: String = "avatarId"
+ @State private var contentOffset = CGFloat(0)
+ @State private var initialOffset = CGFloat(0)
+
+ var isHeaderVisible: Bool {
+ if !viewModel.searchTerm.isEmpty {
+ return true
+ }
+
+ let threshold = initialOffset + 15
+ return viewModel.filteredCiphers.count > 1 && contentOffset > threshold
+ }
+
+ var body: some View {
+ NavigationView {
+ GeometryReader { geometry in
+ ScrollViewReader { scrollProxy in
+ TrackableWithHeaderListView { offset in
+ withAnimation {
+ contentOffset = offset?.y ?? 0
+ }
+ } headerContent: {
+ Section() {
+ ZStack {
+ searchContent
+ .padding(5)
+ .background(
+ RoundedRectangle(cornerRadius: 5)
+ .foregroundColor(Color.ui.primary)
+ .frame(width: geometry.size.width,
+ alignment: .leading)
+ )
+ .opacity(isHeaderVisible ? 1 : 0)
+ }
+ .background(
+ RoundedRectangle(cornerRadius: 5)
+ .foregroundColor(Color.black)
+ .frame(width: geometry.size.width, height: 60)
+ )
+ .offset(y:isHeaderVisible ? 0 : 5)
+ .padding(0)
+ }
+ } content: {
+ if viewModel.user?.email != nil {
+ Section() {
+ avatarHeader
+ .id(AVATAR_ID)
+ }
+ }
+ ForEach(viewModel.filteredCiphers, id: \.id) { cipher in
+ NavigationLink(destination: CipherDetailsView(cipher: cipher)){
+ CipherItemView(cipher, geometry.size.width)
+ }
+ .listRowInsets(EdgeInsets())
+ .listRowBackground(Color.clear)
+ .padding(3)
+ }
+ }
+ .emptyState(viewModel.filteredCiphers.isEmpty, emptyContent: {
+ emptyContent
+ .frame(width: geometry.size.width, alignment: .center)
+ })
+ .onReceive(self.viewModel.$updateHack) { _ in
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
+ scrollProxy.scrollTo(AVATAR_ID, anchor: .top)
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15){
+ self.initialOffset = self.contentOffset
+ }
+ }
+ }
+ }
+ }
+ }
+ .onAppear {
+ self.viewModel.checkStateAndFetch()
+ }
+ .fullScreenCover(isPresented: $viewModel.showingSheet) {
+ BWStateView(viewModel.currentState)
+ }
+ }
+
+ var searchContent: some View {
+ HStack {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(Color(.white))
+ .frame(width: 20, height: 30)
+ TextField("", text: $viewModel.searchTerm)
+ .foregroundColor(.white)
+ .frame(width: .infinity, height: 33)
+ .placeholder(when: viewModel.searchTerm.isEmpty) {
+ Text("Search").foregroundColor(.white)
+ }
+ }
+ }
+
+ var avatarHeader: some View {
+ HStack {
+ AvatarView(viewModel.user)
+ Text(viewModel.user!.email!)
+ .font(.headline)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ }
+ }
+
+ var emptyContent: some View {
+ VStack(alignment: .center) {
+ Image("EmptyListPlaceholder")
+ .resizable()
+ .scaledToFit()
+ .padding(20)
+ Text("ThereAreNoItemsToList")
+ .foregroundColor(Color.white)
+ .font(.headline)
+ .multilineTextAlignment(.center)
+ }
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ var v = CipherListView()
+ StateService.shared.currentState = .valid
+ v.viewModel = CipherListViewModel(CipherServiceMock())
+ v.viewModel.user = User(id: "zxc", email: "testing@test.com", name: "Tester")
+ return v
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/WatchConnectivityManager.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/WatchConnectivityManager.swift
new file mode 100644
index 000000000..03908d36f
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/WatchConnectivityManager.swift
@@ -0,0 +1,116 @@
+import Combine
+import Foundation
+import WatchConnectivity
+
+struct WatchConnectivityMessage {
+ var state: BWState?
+}
+
+final class WatchConnectivityManager: NSObject, ObservableObject {
+ static let shared = WatchConnectivityManager()
+
+ let watchConnectivitySubject = CurrentValueSubject(WatchConnectivityMessage(state: nil))
+
+ private let kMessageKey = "message"
+ private let kCipherDataKey = "watchDto"
+
+ private override init() {
+ super.init()
+
+ if WCSession.isSupported() {
+ WCSession.default.delegate = self
+ WCSession.default.activate()
+ }
+ }
+
+ var isSessionActivated: Bool {
+ return WCSession.default.isCompanionAppInstalled && WCSession.default.activationState == .activated
+ }
+
+ func send(_ message: String) {
+ guard WCSession.default.activationState == .activated else {
+ return
+ }
+
+ guard WCSession.default.isCompanionAppInstalled else {
+ return
+ }
+
+ WCSession.default.sendMessage([kMessageKey : message], replyHandler: nil) { error in
+ Log.e("Cannot send message: \(String(describing: error))")
+ }
+ }
+}
+
+extension WatchConnectivityManager: WCSessionDelegate {
+ func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
+ }
+
+ func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
+ }
+
+ func session(_ session: WCSession,
+ activationDidCompleteWith activationState: WCSessionActivationState,
+ error: Error?) {
+ }
+
+ func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
+ // in order for the delivery to be faster the time is added to the key to make each application context update have a different key
+ // and update faster
+ let watchDtoKey = applicationContext.keys.first { k in
+ k.starts(with: kCipherDataKey)
+ }
+
+ guard let dtoKey = watchDtoKey, let serializedDto = applicationContext[dtoKey] as? String else {
+ return
+ }
+
+ do {
+ guard let json = try! JSONSerialization.jsonObject(with: serializedDto.data(using: .utf8)!, options: [.fragmentsAllowed]) as? String else {
+ return
+ }
+
+ let decoder = JSONDecoder()
+ decoder.keyDecodingStrategy = .upperToLowerCamelCase
+ let watchDTO = try decoder.decode(WatchDTO.self, from: json.data(using: .utf8)!)
+
+ let previousUserId = StateService.shared.getUser()?.id
+
+ if previousUserId != watchDTO.userData?.id {
+ self.watchConnectivitySubject.send(WatchConnectivityMessage(state: .syncing))
+ }
+
+ StateService.shared.currentState = watchDTO.state
+ StateService.shared.setUser(user: watchDTO.userData)
+// StateService.shared.setVaultTimeout(watchDTO.settingsData?.vaultTimeoutInMinutes, watchDTO.settingsData?.vaultTimeoutAction ?? .lock)
+ EnvironmentService.shared.baseUrl = watchDTO.environmentData?.base
+ EnvironmentService.shared.setIconsUrl(url: watchDTO.environmentData?.icons)
+
+ if watchDTO.state.isDestructive {
+ CipherService.shared.deleteAll(nil) {
+ self.watchConnectivitySubject.send(WatchConnectivityMessage(state: nil))
+ }
+ }
+
+ if watchDTO.state == .valid, var ciphers = watchDTO.ciphers {
+ // we need to track the to which user the ciphers belong to, so we add the user here to all ciphers
+ // note: it's not being sent directly from the phone to increase performance on the communication
+ ciphers.indices.forEach { i in
+ ciphers[i].userId = watchDTO.userData!.id
+ }
+
+ CipherService.shared.saveCiphers(ciphers) {
+ if let previousUserId = previousUserId,
+ let currentUserid = watchDTO.userData?.id,
+ previousUserId != currentUserid {
+ CipherService.shared.deleteAll(previousUserId) {}
+ }
+ self.watchConnectivitySubject.send(WatchConnectivityMessage(state: nil))
+ }
+ }
+ }
+ catch {
+ Log.e(error)
+ }
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/bitwardenApp.swift b/src/watchOS/bitwarden/bitwarden WatchKit Extension/bitwardenApp.swift
new file mode 100644
index 000000000..a10645018
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/bitwardenApp.swift
@@ -0,0 +1,14 @@
+import SwiftUI
+
+@main
+struct bitwardenApp: App {
+ @SceneBuilder var body: some Scene {
+ WindowGroup {
+ NavigationView {
+ CipherListView()
+ }
+ }
+
+ WKNotificationScene(controller: NotificationController.self, category: "myCategory")
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/project.pbxproj b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..1a54dc894
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.pbxproj
@@ -0,0 +1,987 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 55;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1B0A1A9E28ECD7C400FF61CD /* WatchConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B0A1A9D28ECD7C400FF61CD /* WatchConnectivityManager.swift */; };
+ 1B11C899291BFAB500CE58D8 /* CryptoFunctionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B11C898291BFAB500CE58D8 /* CryptoFunctionService.swift */; };
+ 1B11C89B291C587600CE58D8 /* UInt64Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B11C89A291C587600CE58D8 /* UInt64Extensions.swift */; };
+ 1B14DF37291186D900EA43F1 /* EmptyStateViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B14DF36291186D900EA43F1 /* EmptyStateViewModifier.swift */; };
+ 1B15613328B7F3D400610B9B /* bitwardenApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15613228B7F3D400610B9B /* bitwardenApp.swift */; };
+ 1B15613528B7F3D400610B9B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15613428B7F3D400610B9B /* ContentView.swift */; };
+ 1B15613728B7F3D700610B9B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15613628B7F3D700610B9B /* Assets.xcassets */; };
+ 1B15613A28B7F3D700610B9B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15613928B7F3D700610B9B /* Preview Assets.xcassets */; };
+ 1B15613E28B7F3D700610B9B /* Bitwarden.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 1B15613D28B7F3D700610B9B /* Bitwarden.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 1B15614328B7F3D800610B9B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15614228B7F3D800610B9B /* Assets.xcassets */; };
+ 1B15614928B7F3D800610B9B /* bitwarden WatchKit Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 1B15614E28B7F3D800610B9B /* bitwardenApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15614D28B7F3D800610B9B /* bitwardenApp.swift */; };
+ 1B15615028B7F3D800610B9B /* CipherListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15614F28B7F3D800610B9B /* CipherListView.swift */; };
+ 1B15615228B7F3D800610B9B /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15615128B7F3D800610B9B /* NotificationController.swift */; };
+ 1B15615428B7F3D800610B9B /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15615328B7F3D800610B9B /* NotificationView.swift */; };
+ 1B15615628B7F3D800610B9B /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15615528B7F3D800610B9B /* ComplicationController.swift */; };
+ 1B15615828B7F3D900610B9B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15615728B7F3D900610B9B /* Assets.xcassets */; };
+ 1B15615B28B7F3D900610B9B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */; };
+ 1B15616C28B81A2200610B9B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616B28B81A2200610B9B /* Cipher.swift */; };
+ 1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */; };
+ 1B59EC5729007DEE00A8718D /* BitwardenDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */; };
+ 1B59EC592900801500A8718D /* StringEncryptionTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */; };
+ 1B59EC5C2900BB3400A8718D /* CryptoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC5B2900BB3400A8718D /* CryptoService.swift */; };
+ 1B59EC612900C48E00A8718D /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC602900C48E00A8718D /* KeychainHelper.swift */; };
+ 1B59EC632901B1C100A8718D /* LoggerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B59EC622901B1C100A8718D /* LoggerHelper.swift */; };
+ 1B5AFF0329196C81004478F9 /* ColorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5AFF0229196C81004478F9 /* ColorUtils.swift */; };
+ 1B5AFF0729197822004478F9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1B5AFF0929197822004478F9 /* Localizable.strings */; };
+ 1B5F5E38293F9CF8009B5FCC /* TrackableWithHeaderListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5F5E37293F9CF8009B5FCC /* TrackableWithHeaderListView.swift */; };
+ 1B5F5E3A293F9D6F009B5FCC /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */; };
+ 1B5F5E3E293FBB17009B5FCC /* CipherItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5F5E3D293FBB17009B5FCC /* CipherItemView.swift */; };
+ 1B6BD10229364F020041982D /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6BD10129364F020041982D /* AvatarView.swift */; };
+ 1B8453EC290C672E00F921E1 /* CipherEntity+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8453EA290C672E00F921E1 /* CipherEntity+CoreDataClass.swift */; };
+ 1B8453ED290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8453EB290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift */; };
+ 1B8BF90429199BBB006F069E /* CipherDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90329199BBB006F069E /* CipherDetailsView.swift */; };
+ 1B8BF90629199EC5006F069E /* CipherDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90529199EC5006F069E /* CipherDetailsViewModel.swift */; };
+ 1B8BF9092919A2CC006F069E /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF9082919A2CC006F069E /* CircularProgressView.swift */; };
+ 1B8BF90B2919AF2A006F069E /* TotpService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90A2919AF2A006F069E /* TotpService.swift */; };
+ 1B8BF90D2919BED9006F069E /* Base32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF90C2919BED9006F069E /* Base32.swift */; };
+ 1B8BF9112919CDBB006F069E /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8BF9102919CDBB006F069E /* DateExtensions.swift */; };
+ 1BC1CD6329227D3C006540DA /* EnvironmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6229227D3C006540DA /* EnvironmentService.swift */; };
+ 1BC1CD6529227F3C006540DA /* IconImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6429227F3C006540DA /* IconImageHelper.swift */; };
+ 1BC1CD672922871A006540DA /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD662922871A006540DA /* URLExtensions.swift */; };
+ 1BC1CD6929228CEB006540DA /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6829228CEB006540DA /* StringExtensions.swift */; };
+ 1BC1CD6C29229D1B006540DA /* CipherMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6B29229D1B006540DA /* CipherMock.swift */; };
+ 1BC1CD6E2922B92B006540DA /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC1CD6D2922B92B006540DA /* ImageView.swift */; };
+ 1BD291B32924043C0004F33F /* BWStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B22924043C0004F33F /* BWStateView.swift */; };
+ 1BD291B52924047C0004F33F /* BWStateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B42924047C0004F33F /* BWStateViewModel.swift */; };
+ 1BD291B7292409410004F33F /* BWState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B6292409410004F33F /* BWState.swift */; };
+ 1BD291B9292438830004F33F /* StateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291B8292438830004F33F /* StateService.swift */; };
+ 1BD291BB2927E9B50004F33F /* WatchDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291BA2927E9B50004F33F /* WatchDTO.swift */; };
+ 1BD291BD292807240004F33F /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291BC292807240004F33F /* User.swift */; };
+ 1BD291BF292D0E6F0004F33F /* JsonDecoderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */; };
+ 1BD291C1292E7E690004F33F /* ErrorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291C0292E7E690004F33F /* ErrorExtensions.swift */; };
+ 1BD291C329311E1C0004F33F /* VaultTimeoutAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD291C229311E1C0004F33F /* VaultTimeoutAction.swift */; };
+ 1BDBFEAC290B4215009C78C7 /* CipherListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BDBFEAB290B4215009C78C7 /* CipherListViewModel.swift */; };
+ 1BDBFEB1290B5BD3009C78C7 /* DBHelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BDBFEB0290B5BD3009C78C7 /* DBHelperProtocol.swift */; };
+ 1BDBFEB3290B5D07009C78C7 /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BDBFEB2290B5D07009C78C7 /* CoreDataHelper.swift */; };
+ 1BF5F6DB29103066002DDC0C /* CipherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF5F6DA29103066002DDC0C /* CipherService.swift */; };
+ 1BF5F6DE29103B86002DDC0C /* CipherServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF5F6DD29103B86002DDC0C /* CipherServiceMock.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 1B15613F28B7F3D700610B9B /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 1B15612728B7F3D400610B9B /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 1B15613C28B7F3D700610B9B;
+ remoteInfo = "bitwarden WatchKit App";
+ };
+ 1B15614A28B7F3D800610B9B /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 1B15612728B7F3D400610B9B /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 1B15614728B7F3D800610B9B;
+ remoteInfo = "bitwarden WatchKit Extension";
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 1B15616328B7F3D900610B9B /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 1B15614928B7F3D800610B9B /* bitwarden WatchKit Extension.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 1B15616728B7F3D900610B9B /* Embed Watch Content */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
+ dstSubfolderSpec = 16;
+ files = (
+ 1B15613E28B7F3D700610B9B /* Bitwarden.app in Embed Watch Content */,
+ );
+ name = "Embed Watch Content";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1B0A1A9D28ECD7C400FF61CD /* WatchConnectivityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchConnectivityManager.swift; sourceTree = ""; };
+ 1B11C898291BFAB500CE58D8 /* CryptoFunctionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFunctionService.swift; sourceTree = ""; };
+ 1B11C89A291C587600CE58D8 /* UInt64Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt64Extensions.swift; sourceTree = ""; };
+ 1B14DF36291186D900EA43F1 /* EmptyStateViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateViewModifier.swift; sourceTree = ""; };
+ 1B15612F28B7F3D400610B9B /* bitwarden.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitwarden.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1B15613228B7F3D400610B9B /* bitwardenApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = bitwardenApp.swift; sourceTree = ""; };
+ 1B15613428B7F3D400610B9B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 1B15613628B7F3D700610B9B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 1B15613928B7F3D700610B9B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 1B15613D28B7F3D700610B9B /* Bitwarden.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bitwarden.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1B15614228B7F3D800610B9B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "bitwarden WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1B15614D28B7F3D800610B9B /* bitwardenApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = bitwardenApp.swift; sourceTree = ""; };
+ 1B15614F28B7F3D800610B9B /* CipherListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherListView.swift; sourceTree = ""; };
+ 1B15615128B7F3D800610B9B /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; };
+ 1B15615328B7F3D800610B9B /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; };
+ 1B15615528B7F3D800610B9B /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = ""; };
+ 1B15615728B7F3D900610B9B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 1B15615C28B7F3D900610B9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 1B15615D28B7F3D900610B9B /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = ""; };
+ 1B15616B28B81A2200610B9B /* Cipher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = ""; };
+ 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnectivityManager.swift; sourceTree = ""; };
+ 1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = BitwardenDB.xcdatamodel; sourceTree = ""; };
+ 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringEncryptionTransformer.swift; sourceTree = ""; };
+ 1B59EC5B2900BB3400A8718D /* CryptoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoService.swift; sourceTree = ""; };
+ 1B59EC602900C48E00A8718D /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; };
+ 1B59EC622901B1C100A8718D /* LoggerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerHelper.swift; sourceTree = ""; };
+ 1B5AFF0229196C81004478F9 /* ColorUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorUtils.swift; sourceTree = ""; };
+ 1B5AFF0829197822004478F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
+ 1B5F5E37293F9CF8009B5FCC /* TrackableWithHeaderListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackableWithHeaderListView.swift; sourceTree = ""; };
+ 1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; };
+ 1B5F5E3D293FBB17009B5FCC /* CipherItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherItemView.swift; sourceTree = ""; };
+ 1B6BD10129364F020041982D /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; };
+ 1B8453EA290C672E00F921E1 /* CipherEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CipherEntity+CoreDataClass.swift"; sourceTree = ""; };
+ 1B8453EB290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CipherEntity+CoreDataProperties.swift"; sourceTree = ""; };
+ 1B8BF90329199BBB006F069E /* CipherDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherDetailsView.swift; sourceTree = ""; };
+ 1B8BF90529199EC5006F069E /* CipherDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherDetailsViewModel.swift; sourceTree = ""; };
+ 1B8BF9082919A2CC006F069E /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; };
+ 1B8BF90A2919AF2A006F069E /* TotpService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotpService.swift; sourceTree = ""; };
+ 1B8BF90C2919BED9006F069E /* Base32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base32.swift; sourceTree = ""; };
+ 1B8BF9102919CDBB006F069E /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; };
+ 1BC1CD6229227D3C006540DA /* EnvironmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentService.swift; sourceTree = ""; };
+ 1BC1CD6429227F3C006540DA /* IconImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImageHelper.swift; sourceTree = ""; };
+ 1BC1CD662922871A006540DA /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; };
+ 1BC1CD6829228CEB006540DA /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; };
+ 1BC1CD6B29229D1B006540DA /* CipherMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherMock.swift; sourceTree = ""; };
+ 1BC1CD6D2922B92B006540DA /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; };
+ 1BD291B22924043C0004F33F /* BWStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWStateView.swift; sourceTree = ""; };
+ 1BD291B42924047C0004F33F /* BWStateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWStateViewModel.swift; sourceTree = ""; };
+ 1BD291B6292409410004F33F /* BWState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWState.swift; sourceTree = ""; };
+ 1BD291B8292438830004F33F /* StateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateService.swift; sourceTree = ""; };
+ 1BD291BA2927E9B50004F33F /* WatchDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchDTO.swift; sourceTree = ""; };
+ 1BD291BC292807240004F33F /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
+ 1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDecoderExtensions.swift; sourceTree = ""; };
+ 1BD291C0292E7E690004F33F /* ErrorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorExtensions.swift; sourceTree = ""; };
+ 1BD291C229311E1C0004F33F /* VaultTimeoutAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultTimeoutAction.swift; sourceTree = ""; };
+ 1BDBFEAB290B4215009C78C7 /* CipherListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherListViewModel.swift; sourceTree = ""; };
+ 1BDBFEB0290B5BD3009C78C7 /* DBHelperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBHelperProtocol.swift; sourceTree = ""; };
+ 1BDBFEB2290B5D07009C78C7 /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = ""; };
+ 1BF5F6DA29103066002DDC0C /* CipherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherService.swift; sourceTree = ""; };
+ 1BF5F6DD29103B86002DDC0C /* CipherServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherServiceMock.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 1B15612C28B7F3D400610B9B /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 1B15614528B7F3D800610B9B /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 1B0A1A9C28E77F4500FF61CD /* Models */ = {
+ isa = PBXGroup;
+ children = (
+ 1BC1CD6A29229D12006540DA /* Mocks */,
+ 1B15616B28B81A2200610B9B /* Cipher.swift */,
+ 1BD291BA2927E9B50004F33F /* WatchDTO.swift */,
+ 1BD291BC292807240004F33F /* User.swift */,
+ 1BD291C229311E1C0004F33F /* VaultTimeoutAction.swift */,
+ );
+ path = Models;
+ sourceTree = "";
+ };
+ 1B14DF35291186C300EA43F1 /* Utilities */ = {
+ isa = PBXGroup;
+ children = (
+ 1B14DF36291186D900EA43F1 /* EmptyStateViewModifier.swift */,
+ 1B5AFF0229196C81004478F9 /* ColorUtils.swift */,
+ 1B8BF90C2919BED9006F069E /* Base32.swift */,
+ 1B8BF9102919CDBB006F069E /* DateExtensions.swift */,
+ 1B11C89A291C587600CE58D8 /* UInt64Extensions.swift */,
+ 1BC1CD662922871A006540DA /* URLExtensions.swift */,
+ 1BC1CD6829228CEB006540DA /* StringExtensions.swift */,
+ 1BD291B6292409410004F33F /* BWState.swift */,
+ 1BD291BE292D0E6F0004F33F /* JsonDecoderExtensions.swift */,
+ 1BD291C0292E7E690004F33F /* ErrorExtensions.swift */,
+ 1B5F5E39293F9D6F009B5FCC /* ViewExtensions.swift */,
+ );
+ path = Utilities;
+ sourceTree = "";
+ };
+ 1B15612628B7F3D400610B9B = {
+ isa = PBXGroup;
+ children = (
+ 1B15613128B7F3D400610B9B /* bitwarden */,
+ 1B15614128B7F3D700610B9B /* bitwarden WatchKit App */,
+ 1B15614C28B7F3D800610B9B /* bitwarden WatchKit Extension */,
+ 1B15613028B7F3D400610B9B /* Products */,
+ );
+ sourceTree = "";
+ };
+ 1B15613028B7F3D400610B9B /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 1B15612F28B7F3D400610B9B /* bitwarden.app */,
+ 1B15613D28B7F3D700610B9B /* Bitwarden.app */,
+ 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 1B15613128B7F3D400610B9B /* bitwarden */ = {
+ isa = PBXGroup;
+ children = (
+ 1B0A1A9D28ECD7C400FF61CD /* WatchConnectivityManager.swift */,
+ 1B15613228B7F3D400610B9B /* bitwardenApp.swift */,
+ 1B15613428B7F3D400610B9B /* ContentView.swift */,
+ 1B15613628B7F3D700610B9B /* Assets.xcassets */,
+ 1B15613828B7F3D700610B9B /* Preview Content */,
+ );
+ path = bitwarden;
+ sourceTree = "";
+ };
+ 1B15613828B7F3D700610B9B /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 1B15613928B7F3D700610B9B /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 1B15614128B7F3D700610B9B /* bitwarden WatchKit App */ = {
+ isa = PBXGroup;
+ children = (
+ 1B15614228B7F3D800610B9B /* Assets.xcassets */,
+ );
+ path = "bitwarden WatchKit App";
+ sourceTree = "";
+ };
+ 1B15614C28B7F3D800610B9B /* bitwarden WatchKit Extension */ = {
+ isa = PBXGroup;
+ children = (
+ 1B8BF9072919A2BC006F069E /* Controls */,
+ 1B5AFF0629197809004478F9 /* Localization */,
+ 1B59EC5F2900C48300A8718D /* Helpers */,
+ 1B59EC5A2900BB2900A8718D /* Services */,
+ 1BDBFEAD290B4591009C78C7 /* DataStorage */,
+ 1B59EC5429007D7300A8718D /* Entities */,
+ 1B0A1A9C28E77F4500FF61CD /* Models */,
+ 1BDBFEA9290851AB009C78C7 /* Views */,
+ 1BDBFEAA290B41FE009C78C7 /* ViewModels */,
+ 1B14DF35291186C300EA43F1 /* Utilities */,
+ 1B15614D28B7F3D800610B9B /* bitwardenApp.swift */,
+ 1B15615128B7F3D800610B9B /* NotificationController.swift */,
+ 1B15615328B7F3D800610B9B /* NotificationView.swift */,
+ 1B15615528B7F3D800610B9B /* ComplicationController.swift */,
+ 1B15615728B7F3D900610B9B /* Assets.xcassets */,
+ 1B15615C28B7F3D900610B9B /* Info.plist */,
+ 1B15615D28B7F3D900610B9B /* PushNotificationPayload.apns */,
+ 1B15615928B7F3D900610B9B /* Preview Content */,
+ 1B15616D28B81A4300610B9B /* WatchConnectivityManager.swift */,
+ );
+ path = "bitwarden WatchKit Extension";
+ sourceTree = "";
+ };
+ 1B15615928B7F3D900610B9B /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 1B15615A28B7F3D900610B9B /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 1B59EC5429007D7300A8718D /* Entities */ = {
+ isa = PBXGroup;
+ children = (
+ 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */,
+ 1B8453EA290C672E00F921E1 /* CipherEntity+CoreDataClass.swift */,
+ 1B8453EB290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift */,
+ 1B59EC582900801500A8718D /* StringEncryptionTransformer.swift */,
+ );
+ path = Entities;
+ sourceTree = "";
+ };
+ 1B59EC5A2900BB2900A8718D /* Services */ = {
+ isa = PBXGroup;
+ children = (
+ 1BF5F6DC29103B5F002DDC0C /* Mocks */,
+ 1B59EC5B2900BB3400A8718D /* CryptoService.swift */,
+ 1BF5F6DA29103066002DDC0C /* CipherService.swift */,
+ 1B8BF90A2919AF2A006F069E /* TotpService.swift */,
+ 1B11C898291BFAB500CE58D8 /* CryptoFunctionService.swift */,
+ 1BC1CD6229227D3C006540DA /* EnvironmentService.swift */,
+ 1BD291B8292438830004F33F /* StateService.swift */,
+ );
+ path = Services;
+ sourceTree = "";
+ };
+ 1B59EC5F2900C48300A8718D /* Helpers */ = {
+ isa = PBXGroup;
+ children = (
+ 1B59EC602900C48E00A8718D /* KeychainHelper.swift */,
+ 1B59EC622901B1C100A8718D /* LoggerHelper.swift */,
+ 1BC1CD6429227F3C006540DA /* IconImageHelper.swift */,
+ );
+ path = Helpers;
+ sourceTree = "";
+ };
+ 1B5AFF0629197809004478F9 /* Localization */ = {
+ isa = PBXGroup;
+ children = (
+ 1B5AFF0929197822004478F9 /* Localizable.strings */,
+ );
+ path = Localization;
+ sourceTree = "";
+ };
+ 1B8BF9072919A2BC006F069E /* Controls */ = {
+ isa = PBXGroup;
+ children = (
+ 1B8BF9082919A2CC006F069E /* CircularProgressView.swift */,
+ 1BC1CD6D2922B92B006540DA /* ImageView.swift */,
+ 1B6BD10129364F020041982D /* AvatarView.swift */,
+ 1B5F5E37293F9CF8009B5FCC /* TrackableWithHeaderListView.swift */,
+ );
+ path = Controls;
+ sourceTree = "";
+ };
+ 1BC1CD6A29229D12006540DA /* Mocks */ = {
+ isa = PBXGroup;
+ children = (
+ 1BC1CD6B29229D1B006540DA /* CipherMock.swift */,
+ );
+ path = Mocks;
+ sourceTree = "";
+ };
+ 1BDBFEA9290851AB009C78C7 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 1B15614F28B7F3D800610B9B /* CipherListView.swift */,
+ 1B8BF90329199BBB006F069E /* CipherDetailsView.swift */,
+ 1BD291B22924043C0004F33F /* BWStateView.swift */,
+ 1B5F5E3D293FBB17009B5FCC /* CipherItemView.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ 1BDBFEAA290B41FE009C78C7 /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ 1BDBFEAB290B4215009C78C7 /* CipherListViewModel.swift */,
+ 1B8BF90529199EC5006F069E /* CipherDetailsViewModel.swift */,
+ 1BD291B42924047C0004F33F /* BWStateViewModel.swift */,
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ 1BDBFEAD290B4591009C78C7 /* DataStorage */ = {
+ isa = PBXGroup;
+ children = (
+ 1BDBFEB0290B5BD3009C78C7 /* DBHelperProtocol.swift */,
+ 1BDBFEB2290B5D07009C78C7 /* CoreDataHelper.swift */,
+ );
+ path = DataStorage;
+ sourceTree = "";
+ };
+ 1BF5F6DC29103B5F002DDC0C /* Mocks */ = {
+ isa = PBXGroup;
+ children = (
+ 1BF5F6DD29103B86002DDC0C /* CipherServiceMock.swift */,
+ );
+ path = Mocks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 1B15612E28B7F3D400610B9B /* bitwarden */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 1B15616828B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden" */;
+ buildPhases = (
+ 1B15612B28B7F3D400610B9B /* Sources */,
+ 1B15612C28B7F3D400610B9B /* Frameworks */,
+ 1B15612D28B7F3D400610B9B /* Resources */,
+ 1B15616728B7F3D900610B9B /* Embed Watch Content */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 1B15614028B7F3D700610B9B /* PBXTargetDependency */,
+ );
+ name = bitwarden;
+ productName = bitwarden;
+ productReference = 1B15612F28B7F3D400610B9B /* bitwarden.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 1B15613C28B7F3D700610B9B /* bitwarden WatchKit App */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 1B15616428B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit App" */;
+ buildPhases = (
+ 1B15613B28B7F3D700610B9B /* Resources */,
+ 1B15616328B7F3D900610B9B /* Embed Foundation Extensions */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 1B15614B28B7F3D800610B9B /* PBXTargetDependency */,
+ );
+ name = "bitwarden WatchKit App";
+ productName = "bitwarden WatchKit App";
+ productReference = 1B15613D28B7F3D700610B9B /* Bitwarden.app */;
+ productType = "com.apple.product-type.application.watchapp2";
+ };
+ 1B15614728B7F3D800610B9B /* bitwarden WatchKit Extension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 1B15616028B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit Extension" */;
+ buildPhases = (
+ 1B15614428B7F3D800610B9B /* Sources */,
+ 1B15614528B7F3D800610B9B /* Frameworks */,
+ 1B15614628B7F3D800610B9B /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "bitwarden WatchKit Extension";
+ productName = "bitwarden WatchKit Extension";
+ productReference = 1B15614828B7F3D800610B9B /* bitwarden WatchKit Extension.appex */;
+ productType = "com.apple.product-type.watchkit2-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 1B15612728B7F3D400610B9B /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1330;
+ LastUpgradeCheck = 1400;
+ TargetAttributes = {
+ 1B15612E28B7F3D400610B9B = {
+ CreatedOnToolsVersion = 13.3.1;
+ };
+ 1B15613C28B7F3D700610B9B = {
+ CreatedOnToolsVersion = 13.3.1;
+ };
+ 1B15614728B7F3D800610B9B = {
+ CreatedOnToolsVersion = 13.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 1B15612A28B7F3D400610B9B /* Build configuration list for PBXProject "bitwarden" */;
+ compatibilityVersion = "Xcode 13.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 1B15612628B7F3D400610B9B;
+ productRefGroup = 1B15613028B7F3D400610B9B /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 1B15612E28B7F3D400610B9B /* bitwarden */,
+ 1B15613C28B7F3D700610B9B /* bitwarden WatchKit App */,
+ 1B15614728B7F3D800610B9B /* bitwarden WatchKit Extension */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 1B15612D28B7F3D400610B9B /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1B15613A28B7F3D700610B9B /* Preview Assets.xcassets in Resources */,
+ 1B15613728B7F3D700610B9B /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 1B15613B28B7F3D700610B9B /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1B15614328B7F3D800610B9B /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 1B15614628B7F3D800610B9B /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1B15615B28B7F3D900610B9B /* Preview Assets.xcassets in Resources */,
+ 1B5AFF0729197822004478F9 /* Localizable.strings in Resources */,
+ 1B15615828B7F3D900610B9B /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 1B15612B28B7F3D400610B9B /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1B15613528B7F3D400610B9B /* ContentView.swift in Sources */,
+ 1B0A1A9E28ECD7C400FF61CD /* WatchConnectivityManager.swift in Sources */,
+ 1B15613328B7F3D400610B9B /* bitwardenApp.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 1B15614428B7F3D800610B9B /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1B59EC592900801500A8718D /* StringEncryptionTransformer.swift in Sources */,
+ 1BD291B32924043C0004F33F /* BWStateView.swift in Sources */,
+ 1BDBFEAC290B4215009C78C7 /* CipherListViewModel.swift in Sources */,
+ 1BF5F6DB29103066002DDC0C /* CipherService.swift in Sources */,
+ 1B5F5E3E293FBB17009B5FCC /* CipherItemView.swift in Sources */,
+ 1B15616C28B81A2200610B9B /* Cipher.swift in Sources */,
+ 1B8BF90429199BBB006F069E /* CipherDetailsView.swift in Sources */,
+ 1B8BF9112919CDBB006F069E /* DateExtensions.swift in Sources */,
+ 1BD291BB2927E9B50004F33F /* WatchDTO.swift in Sources */,
+ 1B11C899291BFAB500CE58D8 /* CryptoFunctionService.swift in Sources */,
+ 1BC1CD6C29229D1B006540DA /* CipherMock.swift in Sources */,
+ 1B5F5E3A293F9D6F009B5FCC /* ViewExtensions.swift in Sources */,
+ 1B15615228B7F3D800610B9B /* NotificationController.swift in Sources */,
+ 1BD291BD292807240004F33F /* User.swift in Sources */,
+ 1BDBFEB3290B5D07009C78C7 /* CoreDataHelper.swift in Sources */,
+ 1B15615028B7F3D800610B9B /* CipherListView.swift in Sources */,
+ 1BD291BF292D0E6F0004F33F /* JsonDecoderExtensions.swift in Sources */,
+ 1B59EC632901B1C100A8718D /* LoggerHelper.swift in Sources */,
+ 1BD291C329311E1C0004F33F /* VaultTimeoutAction.swift in Sources */,
+ 1B15615628B7F3D800610B9B /* ComplicationController.swift in Sources */,
+ 1BD291B9292438830004F33F /* StateService.swift in Sources */,
+ 1BC1CD6329227D3C006540DA /* EnvironmentService.swift in Sources */,
+ 1B8BF9092919A2CC006F069E /* CircularProgressView.swift in Sources */,
+ 1BD291B7292409410004F33F /* BWState.swift in Sources */,
+ 1B15614E28B7F3D800610B9B /* bitwardenApp.swift in Sources */,
+ 1BC1CD6929228CEB006540DA /* StringExtensions.swift in Sources */,
+ 1BDBFEB1290B5BD3009C78C7 /* DBHelperProtocol.swift in Sources */,
+ 1B8BF90629199EC5006F069E /* CipherDetailsViewModel.swift in Sources */,
+ 1BF5F6DE29103B86002DDC0C /* CipherServiceMock.swift in Sources */,
+ 1BC1CD672922871A006540DA /* URLExtensions.swift in Sources */,
+ 1B11C89B291C587600CE58D8 /* UInt64Extensions.swift in Sources */,
+ 1B8453ED290C672E00F921E1 /* CipherEntity+CoreDataProperties.swift in Sources */,
+ 1BC1CD6E2922B92B006540DA /* ImageView.swift in Sources */,
+ 1BD291C1292E7E690004F33F /* ErrorExtensions.swift in Sources */,
+ 1B14DF37291186D900EA43F1 /* EmptyStateViewModifier.swift in Sources */,
+ 1BD291B52924047C0004F33F /* BWStateViewModel.swift in Sources */,
+ 1B15616E28B81A4300610B9B /* WatchConnectivityManager.swift in Sources */,
+ 1B5AFF0329196C81004478F9 /* ColorUtils.swift in Sources */,
+ 1B59EC612900C48E00A8718D /* KeychainHelper.swift in Sources */,
+ 1B59EC5729007DEE00A8718D /* BitwardenDB.xcdatamodeld in Sources */,
+ 1B8BF90D2919BED9006F069E /* Base32.swift in Sources */,
+ 1B8453EC290C672E00F921E1 /* CipherEntity+CoreDataClass.swift in Sources */,
+ 1BC1CD6529227F3C006540DA /* IconImageHelper.swift in Sources */,
+ 1B59EC5C2900BB3400A8718D /* CryptoService.swift in Sources */,
+ 1B15615428B7F3D800610B9B /* NotificationView.swift in Sources */,
+ 1B6BD10229364F020041982D /* AvatarView.swift in Sources */,
+ 1B8BF90B2919AF2A006F069E /* TotpService.swift in Sources */,
+ 1B5F5E38293F9CF8009B5FCC /* TrackableWithHeaderListView.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 1B15614028B7F3D700610B9B /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 1B15613C28B7F3D700610B9B /* bitwarden WatchKit App */;
+ targetProxy = 1B15613F28B7F3D700610B9B /* PBXContainerItemProxy */;
+ };
+ 1B15614B28B7F3D800610B9B /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 1B15614728B7F3D800610B9B /* bitwarden WatchKit Extension */;
+ targetProxy = 1B15614A28B7F3D800610B9B /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 1B5AFF0929197822004478F9 /* Localizable.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 1B5AFF0829197822004478F9 /* en */,
+ );
+ name = Localizable.strings;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 1B15615E28B7F3D900610B9B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 1B15615F28B7F3D900610B9B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 1B15616128B7F3D900610B9B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"bitwarden WatchKit Extension/Preview Content\"";
+ DEVELOPMENT_TEAM = LTZ2PFU5D6;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "bitwarden WatchKit Extension/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = Bitwarden;
+ INFOPLIST_KEY_CLKComplicationPrincipalClass = bitwarden_WatchKit_Extension.ComplicationController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ MARKETING_VERSION = 2022.8.1;
+ PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp.watchkitextension;
+ PRODUCT_NAME = "${TARGET_NAME}";
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 4;
+ VERSIONING_SYSTEM = "apple-generic";
+ WATCHOS_DEPLOYMENT_TARGET = 8.0;
+ };
+ name = Debug;
+ };
+ 1B15616228B7F3D900610B9B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
+ CODE_SIGN_IDENTITY = "Apple Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"bitwarden WatchKit Extension/Preview Content\"";
+ DEVELOPMENT_TEAM = LTZ2PFU5D6;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "bitwarden WatchKit Extension/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = Bitwarden;
+ INFOPLIST_KEY_CLKComplicationPrincipalClass = bitwarden_WatchKit_Extension.ComplicationController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ MARKETING_VERSION = 2022.8.1;
+ PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp.watchkitextension;
+ PRODUCT_NAME = "${TARGET_NAME}";
+ PROVISIONING_PROFILE_SPECIFIER = "Dist: Bitwarden Watch App Extension";
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 4;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ WATCHOS_DEPLOYMENT_TARGET = 8.0;
+ };
+ name = Release;
+ };
+ 1B15616528B7F3D900610B9B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = LTZ2PFU5D6;
+ GENERATE_INFOPLIST_FILE = YES;
+ IBSC_MODULE = bitwarden_WatchKit_Extension;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.8bit.bitwarden;
+ MARKETING_VERSION = 2022.8.1;
+ PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp;
+ PRODUCT_NAME = Bitwarden;
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 4;
+ VERSIONING_SYSTEM = "apple-generic";
+ WATCHOS_DEPLOYMENT_TARGET = 8.0;
+ };
+ name = Debug;
+ };
+ 1B15616628B7F3D900610B9B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = LTZ2PFU5D6;
+ GENERATE_INFOPLIST_FILE = YES;
+ IBSC_MODULE = bitwarden_WatchKit_Extension;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.8bit.bitwarden;
+ MARKETING_VERSION = 2022.8.1;
+ PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden.watchkitapp;
+ PRODUCT_NAME = Bitwarden;
+ PROVISIONING_PROFILE_SPECIFIER = "Dist: Bitwarden Watch App";
+ SDKROOT = watchos;
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 4;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ WATCHOS_DEPLOYMENT_TARGET = 8.0;
+ };
+ name = Release;
+ };
+ 1B15616928B7F3D900610B9B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"bitwarden/Preview Content\"";
+ DEVELOPMENT_TEAM = LTZ2PFU5D6;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 2022.8.1;
+ PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SDKROOT = iphoneos;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 1B15616A28B7F3D900610B9B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"bitwarden/Preview Content\"";
+ DEVELOPMENT_TEAM = LTZ2PFU5D6;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 2022.8.1;
+ PRODUCT_BUNDLE_IDENTIFIER = com.8bit.bitwarden;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "Dist: Bitwarden 2021";
+ SDKROOT = iphoneos;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 1B15612A28B7F3D400610B9B /* Build configuration list for PBXProject "bitwarden" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1B15615E28B7F3D900610B9B /* Debug */,
+ 1B15615F28B7F3D900610B9B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 1B15616028B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit Extension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1B15616128B7F3D900610B9B /* Debug */,
+ 1B15616228B7F3D900610B9B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 1B15616428B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden WatchKit App" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1B15616528B7F3D900610B9B /* Debug */,
+ 1B15616628B7F3D900610B9B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 1B15616828B7F3D900610B9B /* Build configuration list for PBXNativeTarget "bitwarden" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1B15616928B7F3D900610B9B /* Debug */,
+ 1B15616A28B7F3D900610B9B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCVersionGroup section */
+ 1B59EC5529007DEE00A8718D /* BitwardenDB.xcdatamodeld */ = {
+ isa = XCVersionGroup;
+ children = (
+ 1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */,
+ );
+ currentVersion = 1B59EC5629007DEE00A8718D /* BitwardenDB.xcdatamodel */;
+ path = BitwardenDB.xcdatamodeld;
+ sourceTree = "";
+ versionGroupType = wrapper.xcdatamodel;
+ };
+/* End XCVersionGroup section */
+ };
+ rootObject = 1B15612728B7F3D400610B9B /* Project object */;
+}
diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Complication).xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Complication).xcscheme
new file mode 100644
index 000000000..3b8c13b18
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Complication).xcscheme
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Notification).xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Notification).xcscheme
new file mode 100644
index 000000000..aafffd87d
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App (Notification).xcscheme
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App.xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App.xcscheme
new file mode 100644
index 000000000..2437681c1
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden WatchKit App.xcscheme
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden.xcscheme b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden.xcscheme
new file mode 100644
index 000000000..0023047d2
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden.xcodeproj/xcshareddata/xcschemes/bitwarden.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AccentColor.colorset/Contents.json b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..eb8789700
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..9221b9bb1
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "83.5x83.5"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden/Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden/ContentView.swift b/src/watchOS/bitwarden/bitwarden/ContentView.swift
new file mode 100644
index 000000000..9bcc4c32f
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden/ContentView.swift
@@ -0,0 +1,27 @@
+import SwiftUI
+
+struct ContentView: View {
+ @ObservedObject private var connectivityManager = WatchConnectivityManager.shared
+
+ var body: some View {
+ VStack{
+ Button("Main Tap me!", action: {
+ WatchConnectivityManager.shared.send("From main app")
+ })
+// List(viewModel.ciphers){ cipher in
+// Text("\(cipher.name): \(cipher.login.totp)")
+// .padding()
+// }
+ }
+ .alert(item: $connectivityManager.notificationMessage) { message in
+ Alert(title: Text(message.text),
+ dismissButton: .default(Text("Dismiss")))
+ }
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden/Preview Content/Preview Assets.xcassets/Contents.json b/src/watchOS/bitwarden/bitwarden/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/watchOS/bitwarden/bitwarden/WatchConnectivityManager.swift b/src/watchOS/bitwarden/bitwarden/WatchConnectivityManager.swift
new file mode 100644
index 000000000..9858b5aa9
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden/WatchConnectivityManager.swift
@@ -0,0 +1,126 @@
+import Foundation
+import WatchConnectivity
+
+struct NotificationMessage: Identifiable {
+ let id = UUID()
+ let text: String
+}
+
+final class WatchConnectivityManager: NSObject, ObservableObject {
+ static let shared = WatchConnectivityManager()
+ @Published var notificationMessage: NotificationMessage? = nil
+
+ private let kMessageKey = "message"
+ private let kCipherDataKey = "cipherData"
+
+ private override init() {
+ super.init()
+
+ if WCSession.isSupported() {
+ WCSession.default.delegate = self
+ WCSession.default.activate()
+ }
+ }
+
+ func send(_ message: String) {
+ guard WCSession.default.activationState == .activated else {
+ return
+ }
+ #if os(iOS)
+ guard WCSession.default.isWatchAppInstalled else {
+ return
+ }
+ #else
+ guard WCSession.default.isCompanionAppInstalled else {
+ return
+ }
+ #endif
+
+ guard WCSession.default.isReachable else {
+ return
+ }
+
+ WCSession.default.sendMessage([kMessageKey : message], replyHandler: nil) { error in
+ print("Cannot send message: \(String(describing: error))")
+ }
+ }
+}
+
+extension WatchConnectivityManager: WCSessionDelegate {
+ func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
+ DispatchQueue.main.async { [weak self] in
+ self?.notificationMessage = NotificationMessage(text: "testing this didReceiveMessage")
+ }
+
+ if let notificationText = message[kMessageKey] as? String {
+ DispatchQueue.main.async { [weak self] in
+ self?.notificationMessage = NotificationMessage(text: notificationText)
+ }
+ }
+ }
+
+ func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
+ DispatchQueue.main.async { [weak self] in
+ self?.notificationMessage = NotificationMessage(text: "testing this didReceiveMessage")
+ }
+ let returnMessage: [String : Any] = [
+ "key1" : "s"
+ ]
+
+ replyHandler(returnMessage)
+
+// if let notificationText = message[kMessageKey] as? String {
+// DispatchQueue.main.async { [weak self] in
+// self?.notificationMessage = NotificationMessage(text: notificationText)
+// }
+// }
+
+ }
+
+ func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
+ DispatchQueue.main.async { [weak self] in
+ self?.notificationMessage = NotificationMessage(text: "testing this didReceiveUserInfo")
+ }
+ }
+
+ func session(_ session: WCSession,
+ activationDidCompleteWith activationState: WCSessionActivationState,
+ error: Error?) {
+ DispatchQueue.main.async { [weak self] in
+ self?.notificationMessage = NotificationMessage(text: "testing this activationDidCompleteWith")
+ }
+
+ }
+
+ func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
+ DispatchQueue.main.async { [weak self] in
+ self?.notificationMessage = NotificationMessage(text: "testing this didReceiveApplicationContext")
+ }
+ if let notificationText = applicationContext[kCipherDataKey] as? String {
+
+// let decoder = JSONDecoder()
+// do {
+// let ciphers = try decoder.decode(Cipher.self, from: notificationText.data(using: .utf8)!)
+//
+ DispatchQueue.main.async { [weak self] in
+ let index1 = notificationText.index(notificationText.startIndex, offsetBy: 0)
+ let index2 = notificationText.index(notificationText.startIndex, offsetBy: 6)
+ let indexRange = index1...index2
+ let subString = notificationText[indexRange] // eil
+
+ self?.notificationMessage = NotificationMessage(text: String(subString))
+ }
+// }
+// catch {
+// print(error)
+// }
+ }
+ }
+
+ #if os(iOS)
+ func sessionDidBecomeInactive(_ session: WCSession) {}
+ func sessionDidDeactivate(_ session: WCSession) {
+ session.activate()
+ }
+ #endif
+}
diff --git a/src/watchOS/bitwarden/bitwarden/bitwardenApp.swift b/src/watchOS/bitwarden/bitwarden/bitwardenApp.swift
new file mode 100644
index 000000000..71f943c26
--- /dev/null
+++ b/src/watchOS/bitwarden/bitwarden/bitwardenApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct bitwardenApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}