Skip to content

Commit

Permalink
Improve Handling of noData Import Errors (#2494)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206886138223455/f

**Description**:
Stop prompting the user for feedback when we are handling a noData
error. When no data is found initially, and user chooses to skip manual
import, no summary screen is displayed (the import dialog is dismissed)
  • Loading branch information
aataraxiaa authored Apr 4, 2024
1 parent c1d920c commit 26dfe3f
Show file tree
Hide file tree
Showing 17 changed files with 322 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Error.pdf",
"filename" : "Exclamation-Recolorable-16.pdf",
"idiom" : "universal"
}
],
Expand Down
Binary file not shown.
12 changes: 12 additions & 0 deletions DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Jump-Recolorable-16.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "SuccessCheckmark.pdf",
"filename" : "Check-Recolorable-16.pdf",
"idiom" : "universal"
}
],
Expand Down
Binary file not shown.
7 changes: 7 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,13 @@ struct UserText {
String(format: NSLocalizedString("import.logins.select-csv-file.source", value: "Select %@ CSV File…", comment: "Button text for selecting a CSV file exported from (LastPass or Bitwarden or 1Password - %@)"), source.importSourceName)
}

static func importNoDataBookmarksSubtitle(from source: DataImport.Source) -> String {
String(format: NSLocalizedString("import.nodata.bookmarks.subtitle", value: "If you have %@ bookmarks, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file."), source.importSourceName)
}
static func importNoDataPasswordsSubtitle(from source: DataImport.Source) -> String {
String(format: NSLocalizedString("import.nodata.passwords.subtitle", value: "If you have %@ passwords, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import passwords manually by selecting a CSV or HTML file. The placeholder here represents the source browser, e.g Firefox"), source.importSourceName)
}

static let importLoginsPasswords = NSLocalizedString("import.logins.passwords", value: "Passwords", comment: "Title text for the Passwords import option")

static let importBookmarksButtonTitle = NSLocalizedString("bookmarks.import.button.title", value: "Import", comment: "Button text to open bookmark import dialog")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ final class FirefoxBookmarksReader {
case fetchAllFolders

case copyTemporaryFile

case couldNotFindBookmarksFile
}

var action: DataImportAction { .bookmarks }
Expand All @@ -53,7 +55,7 @@ final class FirefoxBookmarksReader {

var errorType: DataImport.ErrorType {
switch type {
case .dbOpen: .noData
case .dbOpen, .couldNotFindBookmarksFile: .noData
case .fetchRootEntries, .noRootEntries, .fetchTopLevelFolders, .fetchAllBookmarks, .fetchAllFolders: .dataCorrupted
case .copyTemporaryFile: .other
}
Expand All @@ -72,6 +74,10 @@ final class FirefoxBookmarksReader {
}

func readBookmarks() -> DataImportResult<ImportedBookmarks> {
guard FileManager.default.fileExists(atPath: firefoxPlacesDatabaseURL.path) else {
return .failure(ImportError(type: .couldNotFindBookmarksFile, underlyingError: nil))
}

do {
currentOperationType = .copyTemporaryFile
return try firefoxPlacesDatabaseURL.withTemporaryFile { temporaryDatabaseURL in
Expand Down
8 changes: 5 additions & 3 deletions DuckDuckGo/DataImport/Logins/Firefox/FirefoxLoginReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ final class FirefoxLoginReader {

case decryptUsername
case decryptPassword

case couldNotFindKeyDB
}

var action: DataImportAction { .passwords }
Expand All @@ -49,7 +51,7 @@ final class FirefoxLoginReader {

var errorType: DataImport.ErrorType {
switch type {
case .couldNotFindLoginsFile, .couldNotReadLoginsFile: .noData
case .couldNotFindLoginsFile, .couldNotFindKeyDB, .couldNotReadLoginsFile: .noData
case .key3readerStage1, .key3readerStage2, .key3readerStage3, .key4readerStage1, .key4readerStage2, .key4readerStage3, .decryptUsername, .decryptPassword: .decryptionError
case .couldNotDetermineFormat: .dataCorrupted
case .requiresPrimaryPassword: .other
Expand Down Expand Up @@ -94,7 +96,7 @@ final class FirefoxLoginReader {
func readLogins(dataFormat: DataFormat?) -> DataImportResult<[ImportedLoginCredential]> {
var currentOperationType: ImportError.OperationType = .couldNotFindLoginsFile
do {
let dataFormat = try dataFormat ?? detectLoginFormat() ?? { throw ImportError(type: .couldNotDetermineFormat, underlyingError: nil) }()
let dataFormat = try dataFormat ?? detectLoginFormat() ?? { throw ImportError(type: .couldNotFindKeyDB, underlyingError: nil) }()
let keyData = try getEncryptionKey(dataFormat: dataFormat)
let result = try reallyReadLogins(dataFormat: dataFormat, keyData: keyData, currentOperationType: &currentOperationType)
return .success(result)
Expand All @@ -106,7 +108,7 @@ final class FirefoxLoginReader {
}

func getEncryptionKey() throws -> Data {
let dataFormat = try detectLoginFormat() ?? { throw ImportError(type: .couldNotDetermineFormat, underlyingError: nil) }()
let dataFormat = try detectLoginFormat() ?? { throw ImportError(type: .couldNotFindKeyDB, underlyingError: nil) }()
return try getEncryptionKey(dataFormat: dataFormat)
}

Expand Down
19 changes: 14 additions & 5 deletions DuckDuckGo/DataImport/Model/DataImportViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ struct DataImportViewModel {
self.dataType = dataType
self.result = result
}

static func == (lhs: DataTypeImportResult, rhs: DataTypeImportResult) -> Bool {
lhs.dataType == rhs.dataType &&
lhs.result.description == rhs.result.description
}
}

/// collected import summary for current import operation per selected import source
Expand Down Expand Up @@ -248,6 +253,7 @@ struct DataImportViewModel {
// switch to file import of the failed data type displaying successful import results
nextScreen = .fileImport(dataType: dataType, summary: Set(summary.filter({ $0.value.isSuccess }).keys))
}

Pixel.fire(.dataImportFailed(source: importSource, sourceVersion: importSource.installedAppsMajorVersionDescription(selectedProfile: selectedProfile), error: error))
}
}
Expand Down Expand Up @@ -322,16 +328,19 @@ struct DataImportViewModel {
}

/// Skip button press
@MainActor mutating func skipImport() {
@MainActor mutating func skipImportOrDismiss(using dismiss: @escaping () -> Void) {
if let screen = screenForNextDataTypeRemainingToImport(after: screen.fileImportDataType) {
// skip to next non-imported data type
self.screen = screen
} else if selectedDataTypes.first(where: { error(for: $0) != nil }) != nil {
} else if selectedDataTypes.first(where: {
let error = error(for: $0)
return error != nil && error?.errorType != .noData
}) != nil {
// errors occurred during import: show feedback screen
self.screen = .feedback
} else {
// display total summary
self.screen = .summary(selectedDataTypes)
// When we skip a manual import, and there are no next non-imported data types, we dismiss
self.dismiss(using: dismiss)
}
}

Expand Down Expand Up @@ -671,7 +680,7 @@ extension DataImportViewModel {
initiateImport()

case .skip:
skipImport()
skipImportOrDismiss(using: dismiss)

case .cancel:
importTask?.cancel()
Expand Down
6 changes: 2 additions & 4 deletions DuckDuckGo/DataImport/View/DataImportNoDataView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@ struct DataImportNoDataView: View {
Text("We couldn‘t find any bookmarks.", comment: "Data import error message: Bookmarks weren‘t found.")
.bold()

Text("Try importing bookmarks manually instead.",
comment: "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file.")
Text(UserText.importNoDataBookmarksSubtitle(from: source))

case .passwords:
Text("We couldn‘t find any passwords.", comment: "Data import error message: Passwords weren‘t found.")
.bold()

Text("Try importing passwords manually instead.",
comment: "Data import error subtitle: suggestion to import Passwords manually by selecting a CSV or HTML file.")
Text(UserText.importNoDataPasswordsSubtitle(from: source))
}
}
}
Expand Down
38 changes: 32 additions & 6 deletions DuckDuckGo/DataImport/View/DataImportSummaryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ struct DataImportSummaryView: View {
self.model = model
}

private let zeroString = "0"

var body: some View {
VStack(alignment: .leading, spacing: 8) {
{
Expand Down Expand Up @@ -61,7 +63,7 @@ struct DataImportSummaryView: View {
}
if summary.duplicate > 0 {
HStack {
failureImage()
skippedImage()
Text("Duplicate Bookmarks Skipped:",
comment: "Data import summary format of how many duplicate bookmarks (%lld) were skipped during import.")
+ Text(" " as String)
Expand All @@ -78,18 +80,37 @@ struct DataImportSummaryView: View {
}
}

case (.bookmarks, .failure(let error)) where error.errorType == .noData:
HStack {
skippedImage()
Text("Bookmarks:",
comment: "Data import summary format of how many bookmarks were successfully imported.")
+ Text(" " as String)
+ Text(zeroString).bold()
}

case (.bookmarks, .failure):
HStack {
failureImage()
Text("Bookmark import failed.",
comment: "Data import summary message of failed bookmarks import.")
}

case (.passwords, .failure):
HStack {
failureImage()
Text("Password import failed.",
comment: "Data import summary message of failed passwords import.")
case (.passwords, .failure(let error)):
if error.errorType == .noData {
HStack {
skippedImage()
Text("Passwords:",
comment: "Data import summary format of how many passwords were successfully imported.")
+ Text(" " as String)
+ Text(zeroString).bold()
}
} else {
HStack {
failureImage()
Text("Password import failed.",
comment: "Data import summary message of failed passwords import.")
}
}

case (.passwords, .success(let summary)):
Expand Down Expand Up @@ -126,6 +147,11 @@ private func failureImage() -> some View {
.frame(width: 16, height: 16)
}

private func skippedImage() -> some View {
Image(.skipped)
.frame(width: 16, height: 16)
}

#if DEBUG
#Preview {
VStack {
Expand Down
Loading

0 comments on commit 26dfe3f

Please sign in to comment.