Watch shared code (#313)

This commit is contained in:
Shannon Draeker 2024-01-16 10:01:12 -07:00 committed by GitHub
parent 08b4357e77
commit 0e1ea6e9fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 343 additions and 179 deletions

View File

@ -42,6 +42,7 @@ opt_in_rules:
excluded:
- BitwardenShared/UI/Platform/Application/Support/Generated
- BitwardenWatchApp
- BitwardenWatchShared
- build
- vendor/bundle

View File

@ -5,7 +5,7 @@ struct AvatarView: View {
var textColor = Color.black
var initials = ""
init(_ user: User?) {
init(_ user: UserDTO?) {
let source = user?.name ?? user?.email
var upperCaseText: String? = nil
@ -80,6 +80,12 @@ struct AvatarView: View {
struct AvatarView_Previews: PreviewProvider {
static var previews: some View {
AvatarView(User(id: "zxc", email: "asdfasdf@gmail.com", name: "John Snow"))
AvatarView(
UserDTO(
email: "asdfasdf@gmail.com",
id: "zxc",
name: "John Snow"
)
)
}
}

View File

@ -17,15 +17,21 @@ public extension CipherEntity {
}
extension CipherEntity: Identifiable {
func toCipher() -> Cipher {
var loginUrisArray: [LoginUri]?
func toCipher() -> CipherDTO {
var loginUrisArray: [LoginUriDTO]?
if loginUris != nil {
loginUrisArray = try? JSONDecoder().decode([LoginUri].self, from: loginUris!.data(using: .utf8)!)
loginUrisArray = try? JSONDecoder().decode([LoginUriDTO].self, from: loginUris!.data(using: .utf8)!)
}
return Cipher(id: id,
name: name,
userId: userId,
login: Login(username: username, totp: totp, uris: loginUrisArray))
return CipherDTO(
id: id,
login: LoginDTO(
totp: totp,
uris: loginUrisArray,
username: username
),
name: name,
userId: userId
)
}
}

View File

@ -5,7 +5,7 @@ class IconImageHelper {
private init() {}
func getLoginIconImage(_ cipher: Cipher) -> String? {
func getLoginIconImage(_ cipher: CipherDTO) -> String? {
guard let uris = cipher.login.uris, !uris.isEmpty else {
return nil
}

View File

@ -1,30 +1,6 @@
import CoreData
import Foundation
struct Cipher: Identifiable, Codable {
enum CodingKeys: CodingKey {
case id
case name
case login
}
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 {
extension CipherDTO {
func toCipherEntity(moContext: NSManagedObjectContext) -> CipherEntity {
let entity = CipherEntity(context: moContext)
entity.id = id

View File

@ -1,59 +1,71 @@
import Foundation
enum CipherMock {
static let ciphers: [Cipher] = [
Cipher(
static let ciphers: [CipherDTO] = [
CipherDTO(
id: "0",
name: "MySite",
userId: "123123",
login: Login(
username: "test@testing.com",
login: LoginDTO(
totp: "otpauth://account?period=10&secret=LLLLLLLLLLLLLLLL",
uris: cipherLoginUris
)
uris: cipherLoginUris,
username: "test@testing.com"
),
name: "MySite",
userId: "123123"
),
Cipher(
CipherDTO(
id: "1",
login: LoginDTO(
totp: "LLLLLLLLLLLLLLLL",
uris: cipherLoginUris,
username: "thisisatest@testing.com"
),
name: "GitHub",
userId: "123123",
login: Login(username: "thisisatest@testing.com", totp: "LLLLLLLLLLLLLLLL", uris: cipherLoginUris)
userId: "123123"
),
Cipher(
CipherDTO(
id: "2",
name: "No user",
userId: "123123",
login: Login(
username: nil,
login: LoginDTO(
totp: "otpauth://account?period=10&digits=8&algorithm=sha256&secret=LLLLLLLLLLLLLLLL",
uris: cipherLoginUris
)
uris: cipherLoginUris,
username: nil
),
name: "No user",
userId: "123123"
),
Cipher(
CipherDTO(
id: "3",
name: "Site 2",
userId: "123123",
login: Login(
username: "longtestemail000000@fastmailasdfasdf.com",
login: LoginDTO(
totp: "otpauth://account?period=10&digits=7&algorithm=sha512&secret=LLLLLLLLLLLLLLLL",
uris: cipherLoginUris
)
uris: cipherLoginUris,
username: "longtestemail000000@fastmailasdfasdf.com"
),
name: "Site 2",
userId: "123123"
),
Cipher(
CipherDTO(
id: "4",
login: LoginDTO(
totp: "steam://LLLLLLLLLLLLLLLL",
uris: cipherLoginUris,
username: "user3"
),
name: "Really long name for a site that is used for a totp",
userId: "123123",
login: Login(username: "user3", totp: "steam://LLLLLLLLLLLLLLLL", uris: cipherLoginUris)
userId: "123123"
),
Cipher(
CipherDTO(
id: "5",
login: LoginDTO(
totp: "steam://LLLLLLLLLLLLLLLL",
uris: cipherLoginUris,
username: "u"
),
name: "Short",
userId: "123123",
login: Login(username: "u", totp: "steam://LLLLLLLLLLLLLLLL", uris: cipherLoginUris)
userId: "123123"
),
]
static let cipherLoginUris: [LoginUri] = [
LoginUri(uri: "github.com"),
LoginUri(uri: "example2.com"),
static let cipherLoginUris: [LoginUriDTO] = [
LoginUriDTO(uri: "github.com"),
LoginUriDTO(uri: "example2.com"),
]
}

View File

@ -1,7 +0,0 @@
import Foundation
struct User: Codable {
var id: String
var email: String?
var name: String?
}

View File

@ -1,26 +0,0 @@
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
ciphers = nil
userData = nil
environmentData = nil
}
}
struct EnvironmentUrlDataDto: Codable {
var base: String?
var icons: String?
}
// struct SettingsDataDto : Codable {
// var vaultTimeoutInMinutes: Int?
// var vaultTimeoutAction: VaultTimeoutAction
// }

View File

@ -2,9 +2,9 @@ import CoreData
import Foundation
protocol CipherServiceProtocol {
func getCipher(_ id: String) -> Cipher?
func fetchCiphers(_ withUserId: String?) -> [Cipher]
func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void)
func getCipher(_ id: String) -> CipherDTO?
func fetchCiphers(_ withUserId: String?) -> [CipherDTO]
func saveCiphers(_ ciphers: [CipherDTO], completionHandler: @escaping () -> Void)
func deleteAll(_ withUserId: String?, completionHandler: @escaping () -> Void)
}
@ -15,7 +15,7 @@ class CipherService {
private init() {}
func getCipher(_ id: String) -> Cipher? {
func getCipher(_ id: String) -> CipherDTO? {
let predicate = NSPredicate(
format: "id = %@",
id as CVarArg
@ -33,7 +33,7 @@ class CipherService {
// MARK: - CipherServiceProtocol
extension CipherService: CipherServiceProtocol {
func fetchCiphers(_ withUserId: String?) -> [Cipher] {
func fetchCiphers(_ withUserId: String?) -> [CipherDTO] {
let result: Result<[CipherEntity], Error> = dbHelper.fetch(
CipherEntity.self,
"CipherEntity",
@ -47,11 +47,11 @@ extension CipherService: CipherServiceProtocol {
}
}
func saveCiphers(_ ciphers: [Cipher], completionHandler: @escaping () -> Void) {
func saveCiphers(_ ciphers: [CipherDTO], completionHandler: @escaping () -> Void) {
let cipherIds = ciphers.map(\.id)
deleteAll(ciphers[0].userId, notIn: cipherIds) {
self.dbHelper.insertBatch("CipherEntity", items: ciphers) { item, context in
guard let cipher = item as! Cipher? else { return [:] }
guard let cipher = item as! CipherDTO? else { return [:] }
let c = cipher.toCipherEntity(moContext: context)
guard let data = try? JSONEncoder().encode(c) else {
Log.e("Error converting to data")

View File

@ -1,7 +1,7 @@
import Foundation
class CipherServiceMock: CipherServiceProtocol {
func fetchCiphers(_: String?) -> [Cipher] {
func fetchCiphers(_: String?) -> [CipherDTO] {
ciphers
}
@ -9,15 +9,15 @@ class CipherServiceMock: CipherServiceProtocol {
completionHandler()
}
func getCipher(_ id: String) -> Cipher? {
func getCipher(_ id: String) -> CipherDTO? {
CipherMock.ciphers.first { ci in
ci.id == id
}
}
func saveCiphers(_: [Cipher], completionHandler _: @escaping () -> Void) {}
func saveCiphers(_: [CipherDTO], completionHandler _: @escaping () -> Void) {}
private var ciphers = [Cipher]()
private var ciphers = [CipherDTO]()
init() {
ciphers = CipherMock.ciphers

View File

@ -31,11 +31,11 @@ class StateService {
}
}
func getUser() -> User? {
KeychainHelper.standard.read(CURRENT_USER_KEY, User.self)
func getUser() -> UserDTO? {
KeychainHelper.standard.read(CURRENT_USER_KEY, UserDTO.self)
}
func setUser(user: User?) {
func setUser(user: UserDTO?) {
guard let user else {
KeychainHelper.standard.delete(CURRENT_USER_KEY)
return

View File

@ -2,7 +2,7 @@ import Foundation
import SwiftUI
class CipherDetailsViewModel: ObservableObject {
@Published var cipher: Cipher
@Published var cipher: CipherDTO
@Published var totpFormatted: String = ""
@Published var progress: Double = 1
@ -13,7 +13,7 @@ class CipherDetailsViewModel: ObservableObject {
var period: Int
var timer: Timer? = nil
init(cipher: Cipher) {
init(cipher: CipherDTO) {
self.cipher = cipher
key = cipher.login.totp!
period = TotpService.shared.getPeriodFrom(key)

View File

@ -6,12 +6,12 @@ class CipherListViewModel: ObservableObject {
var cipherService: CipherServiceProtocol
var watchConnectivityManager = WatchConnectivityManager.shared
@Published private var ciphers: [Cipher] = []
@Published var filteredCiphers: [Cipher] = []
@Published private var ciphers: [CipherDTO] = []
@Published var filteredCiphers: [CipherDTO] = []
@Published var updateHack: Bool = false
@Published var showingSheet = false
@Published var currentState = BWState.valid
@Published var user: User?
@Published var user: UserDTO?
@Published var searchTerm: String = ""
@ -36,12 +36,19 @@ class CipherListViewModel: ObservableObject {
}
// WORKAROUND: To display 0 search results
if !searchTerm.isEmpty, returnCiphers.isEmpty {
returnCiphers.append(Cipher(
id: "-1",
name: "NoItemsFound",
login: Login(username: "", totp: "", uris: nil)
))
if !searchTerm.isEmpty,
returnCiphers.isEmpty {
returnCiphers.append(
CipherDTO(
id: "-1",
login: LoginDTO(
totp: "",
uris: nil,
username: ""
),
name: "NoItemsFound"
)
)
}
if searchTerm.isEmpty {
@ -84,7 +91,7 @@ class CipherListViewModel: ObservableObject {
}
}
func cipherContains(_ cipher: Cipher, _: String) -> Bool {
func cipherContains(_ cipher: CipherDTO, _: String) -> Bool {
if searchTerm.isEmpty {
return true
}

View File

@ -6,7 +6,7 @@ struct CipherDetailsView: View {
let iconSize: CGSize = .init(width: 30, height: 30)
init(cipher: Cipher) {
init(cipher: CipherDTO) {
cipherDetailsViewModel = CipherDetailsViewModel(cipher: cipher)
}

View File

@ -1,10 +1,10 @@
import SwiftUI
struct CipherItemView: View {
let cipher: Cipher
let cipher: CipherDTO
let maxWidth: CGFloat
init(_ cipher: Cipher, _ maxWidth: CGFloat) {
init(_ cipher: CipherDTO, _ maxWidth: CGFloat) {
self.cipher = cipher
self.maxWidth = maxWidth
}

View File

@ -129,7 +129,11 @@ struct ContentView_Previews: PreviewProvider {
var v = CipherListView()
StateService.shared.currentState = .valid
v.viewModel = CipherListViewModel(CipherServiceMock())
v.viewModel.user = User(id: "zxc", email: "testing@test.com", name: "Tester")
v.viewModel.user = UserDTO(
email: "testing@test.com",
id: "zxc",
name: "Tester"
)
return v
}
}

View File

@ -84,34 +84,9 @@ extension WatchConnectivityManager: WCSessionDelegate {
return
}
let decoder = MessagePackDecoder()
decoder.userInfo[MessagePackDecoder.dataSpecKey] = DataSpecBuilder()
.append("state")
.appendArray("ciphers", DataSpecBuilder()
.append("id")
.append("name")
.appendObj("login", DataSpecBuilder()
.append("username")
.append("totp")
.appendArray("uris", DataSpecBuilder()
.append("uri")
.build())
.build())
.build())
.appendObj("userData", DataSpecBuilder()
.append("id")
.append("email")
.append("name")
.build())
.appendObj("environmentData", DataSpecBuilder()
.append("base")
.append("icons")
.build())
.build()
let rawData = try nsRawData.decompressed(using: .lzfse)
let watchDTO = try decoder.decode(WatchDTO.self, from: Data(referencing: rawData))
let watchDTO = try MessagePackDecoder().decode(WatchDTO.self, from: Data(referencing: rawData))
let previousUserId = StateService.shared.getUser()?.id

View File

@ -21,8 +21,4 @@ struct AnyCodingKey: CodingKey, Equatable {
}
}
extension AnyCodingKey: Hashable {
var hashValue: Int {
intValue?.hashValue ?? stringValue.hashValue
}
}
extension AnyCodingKey: Hashable {}

View File

@ -6,7 +6,7 @@ public struct DataSpec {
let isArray: Bool
let dataSpecBuilder: DataSpecBuilder?
init(_ name: String, _ isObj: Bool, _ isArray: Bool, _ dataSpecBuilder: DataSpecBuilder?) {
public init(_ name: String, _ isObj: Bool, _ isArray: Bool, _ dataSpecBuilder: DataSpecBuilder?) {
self.name = name
self.isObj = isObj
self.isArray = isArray
@ -18,37 +18,37 @@ public class DataSpecBuilder: NSCopying {
var specs: [DataSpec] = []
var specsIterator: IndexingIterator<[DataSpec]>
init() {
public init() {
specsIterator = IndexingIterator(_elements: [])
}
func append(_ name: String) -> DataSpecBuilder {
public func append(_ name: String) -> DataSpecBuilder {
append(DataSpec(name, false, false, nil))
}
func appendObj(_ name: String, _ dataSpecBuilder: DataSpecBuilder) -> DataSpecBuilder {
public func appendObj(_ name: String, _ dataSpecBuilder: DataSpecBuilder) -> DataSpecBuilder {
append(DataSpec(name, true, false, dataSpecBuilder))
}
func appendArray(_ name: String) -> DataSpecBuilder {
public func appendArray(_ name: String) -> DataSpecBuilder {
append(DataSpec(name, false, true, nil))
}
func appendArray(_ name: String, _ dataSpecBuilder: DataSpecBuilder) -> DataSpecBuilder {
public func appendArray(_ name: String, _ dataSpecBuilder: DataSpecBuilder) -> DataSpecBuilder {
append(DataSpec(name, false, true, dataSpecBuilder))
}
func append(_ spec: DataSpec) -> DataSpecBuilder {
public func append(_ spec: DataSpec) -> DataSpecBuilder {
specs.append(spec)
return self
}
func build() -> DataSpecBuilder {
public func build() -> DataSpecBuilder {
specsIterator = specs.makeIterator()
return self
}
func next() -> DataSpec {
public func next() -> DataSpec {
specsIterator.next()!
}

View File

@ -47,15 +47,15 @@ public final class MessagePackDecoder {
case cast
}
static var nonMatchingFloatDecodingStrategyKey: CodingUserInfoKey {
public static var nonMatchingFloatDecodingStrategyKey: CodingUserInfoKey {
CodingUserInfoKey(rawValue: "nonMatchingFloatDecodingStrategyKey")!
}
static var dataSpecKey: CodingUserInfoKey {
public static var dataSpecKey: CodingUserInfoKey {
CodingUserInfoKey(rawValue: "dataSpecKey")!
}
static var isArrayDataSpecKey: CodingUserInfoKey {
public static var isArrayDataSpecKey: CodingUserInfoKey {
CodingUserInfoKey(rawValue: "isArrayDataSpecKey")!
}
}

View File

@ -1,17 +1,20 @@
import Foundation
enum BWState: Int, Codable {
// MARK: - BWState
/// The state of the watch app.
///
public 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
case needDeviceOwnerAuth = 7
case debug = 255
var isDestructive: Bool {
public var isDestructive: Bool {
self == .needSetup || self == .needLogin || self == .needPremium || self == .need2FAItem
}
}

View File

@ -0,0 +1,106 @@
import Foundation
// MARK: - CipherDTO
/// The simplified cipher model used to communicate between the watch and the main app.
///
public struct CipherDTO: Identifiable, Codable {
// MARK: Properties
/// The id of the cipher.
public var id: String
/// The login model of the cipher (all ciphers in the watch are of type login).
public var login: LoginDTO
/// The name of the cipher.
public var name: String?
/// The user id associated with the cipher.
public var userId: String?
enum CodingKeys: CodingKey {
case id
case name
case login
}
// MARK: Initialization
/// Initializes a `CipherDTO`.
///
/// - Parameters:
/// - id: The id of the cipher.
/// - login: The login model of the cipher (all ciphers in the watch are of type login).
/// - name: The name of the cipher.
/// - userId: The user id associated with the cipher.
///
public init(
id: String,
login: LoginDTO,
name: String? = nil,
userId: String? = nil
) {
self.id = id
self.login = login
self.name = name
self.userId = userId
}
}
// MARK: - LoginDTO
/// The simplified login model used to communicate between the watch and the main app.
///
public struct LoginDTO: Codable {
// MARK: Properties
/// The totp code for the login.
public var totp: String?
/// The list of uri's for the login.
public var uris: [LoginUriDTO]?
/// The login associated with the username.
public var username: String?
// MARK: Initialization
/// Initializes a `LoginDTO`,
///
/// - Parameters:
/// - totp: The totp code for the login.
/// - uris: The list of uri's for the login.
/// - username: The login associated with the username.
///
public init(
totp: String? = nil,
uris: [LoginUriDTO]? = nil,
username: String? = nil
) {
self.totp = totp
self.uris = uris
self.username = username
}
}
// MARK: - LoginUriDTO
/// The simplified login uri used to communicate between the watch and the main app.
///
public struct LoginUriDTO: Codable {
// MARK: Properties
/// The uri of the login.
public var uri: String?
// MARK: Initialization
/// Initializes a `LoginUriDTO`,
///
/// - Parameter uri: The uri of the login.
///
public init(uri: String? = nil) {
self.uri = uri
}
}

View File

@ -0,0 +1,28 @@
import Foundation
// MARK: - EnvironmentUrlDTO
/// The environment data used to communicate between the watch and the main app.
///
public struct EnvironmentUrlDTO: Codable {
// MARK: Properties
/// The base url.
public var base: String?
/// The url used for loading icons.
public var icons: String?
// MARK: Initialization
/// Initializes a `EnvironmentUrlDTO`.
///
/// - Parameters:
/// - base: The base url.
/// - icons: The url used for loading icons.
///
public init(base: String? = nil, icons: String? = nil) {
self.base = base
self.icons = icons
}
}

View File

@ -0,0 +1,32 @@
import Foundation
// MARK: - UserDTO
/// The simplified user model used to communicate between the watch and the main app.
public struct UserDTO: Codable {
// MARK: Properties
/// The user's email.
public var email: String?
/// The user's id.
public var id: String
/// The user's name.
public var name: String?
// MARK: Initialization
/// Initializes a `UserDTO`.
///
/// - Parameters:
/// - email: The user's email.
/// - id: The user's id.
/// - name: The user's name.
///
public init(email: String? = nil, id: String, name: String? = nil) {
self.email = email
self.id = id
self.name = name
}
}

View File

@ -0,0 +1,43 @@
import Foundation
// MARK: - WatchDTO
/// The structure of the data used to communicate between the watch and the main app.
///
public struct WatchDTO: Codable {
// MARK: Properties
/// The state of the watch app.
public var state: BWState
/// The list of ciphers to display.
public var ciphers: [CipherDTO]?
/// The user data.
public var userData: UserDTO?
/// The urls to use.
public var environmentData: EnvironmentUrlDTO?
// MARK: Initialization
/// Initializes a `WatchDTO`.
///
/// - Parameters:
/// - state: The state of the watch app.
/// - ciphers: The list of ciphers to display.
/// - userData: The user data.
/// - environmentData: The urls to use.
///
public init(
state: BWState,
ciphers: [CipherDTO]? = nil,
userData: UserDTO? = nil,
environmentData: EnvironmentUrlDTO? = nil
) {
self.state = state
self.ciphers = ciphers
self.userData = userData
self.environmentData = environmentData
}
}

View File

@ -323,6 +323,7 @@ targets:
optional: true
- path: BitwardenShared/UI/Platform/Application/Support/Generated/Localizations.swift
optional: true
- path: BitwardenWatchShared
dependencies:
- package: BitwardenSdk
- package: Networking
@ -370,8 +371,9 @@ targets:
INFOPLIST_FILE: BitwardenWatchApp/Info.plist
sources:
- path: BitwardenWatchApp
- path: BitwardenWatchShared
- path: BitwardenWatchApp/GoogleService-Info.plist
buildPhase: resources
dependencies:
- package: Firebase
product: FirebaseCrashlytics
product: FirebaseCrashlytics