From e299aed93f25282cd5e78333a399a5895210fc78 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Tue, 13 Feb 2024 12:40:01 +0100 Subject: [PATCH 1/4] add sync UI catalogue and fix strings comments (#2193) Task/Issue URL: https://app.asana.com/0/0/1206542805700560/f **Description**: Add String Catalogue to Sync UI module and to Info.plist. Also makes attributed string in sync UI localisable. --- DuckDuckGo.xcodeproj/project.pbxproj | 12 +- DuckDuckGo/Common/Localizables/UserText.swift | 80 +- DuckDuckGo/Info.plist | 2 +- DuckDuckGo/InfoPlist.xcstrings | 78 ++ DuckDuckGo/Localizable.xcstrings | 448 ++++++- LocalPackages/SyncUI/Package.swift | 3 +- .../Dialogs/SyncWithAnotherDeviceView.swift | 23 +- .../SyncUI/internal/Localizable.xcstrings | 1173 +++++++++++++++++ .../Sources/SyncUI/internal/UserText.swift | 56 +- 9 files changed, 1799 insertions(+), 76 deletions(-) create mode 100644 DuckDuckGo/InfoPlist.xcstrings create mode 100644 LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5197368ca9..1729d7b028 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2165,6 +2165,9 @@ 569277C529DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */; }; 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; + 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; + 56CEE90F2B7A725C00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; + 56CEE9102B7A72FE00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56D145E829E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */; }; 56D145E929E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */; }; 56D145EB29E6C99B00E3488A /* DataImportStatusProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145EA29E6C99B00E3488A /* DataImportStatusProviding.swift */; }; @@ -3750,6 +3753,8 @@ 569277C029DDCBB500B633EF /* HomePageContinueSetUpModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageContinueSetUpModel.swift; sourceTree = ""; }; 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueSetUpModelTests.swift; sourceTree = ""; }; 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarUrlExtensionsTests.swift; sourceTree = ""; }; + 56CEE9092B7A66C500CF10AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDataImportProvider.swift; sourceTree = ""; }; 56D145EA29E6C99B00E3488A /* DataImportStatusProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportStatusProviding.swift; sourceTree = ""; }; 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; @@ -3954,7 +3959,6 @@ AA585D81248FD31100E9A3E2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AA585D83248FD31100E9A3E2 /* BrowserTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserTabViewController.swift; sourceTree = ""; }; AA585D85248FD31400E9A3E2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - AA585D8A248FD31400E9A3E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AA585D8B248FD31400E9A3E2 /* DuckDuckGo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo.entitlements; sourceTree = ""; }; AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; AA585D96248FD31400E9A3E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -6733,7 +6737,8 @@ 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */, 4B2D06642A132F3A00DE1F49 /* NetworkProtectionAppExtension.entitlements */, 4B5F14C42A145D6A0060320F /* NetworkProtectionVPNController.entitlements */, - AA585D8A248FD31400E9A3E2 /* Info.plist */, + 56CEE9092B7A66C500CF10AA /* Info.plist */, + 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */, ); path = DuckDuckGo; sourceTree = ""; @@ -8825,6 +8830,7 @@ 3706FCB6293F65D500E42796 /* dark-shield-dot-mouse-over.json in Resources */, 3706FCB7293F65D500E42796 /* 01_Fire_really_small.json in Resources */, 3706FCB8293F65D500E42796 /* Onboarding.storyboard in Resources */, + 56CEE90F2B7A725C00CF10AA /* InfoPlist.xcstrings in Resources */, 3706FCB9293F65D500E42796 /* FireproofDomains.storyboard in Resources */, 3706FCBA293F65D500E42796 /* clickToLoadConfig.json in Resources */, 3706FCBB293F65D500E42796 /* Downloads.storyboard in Resources */, @@ -8952,6 +8958,7 @@ 4B957BF12AC7AE700062CA31 /* dark-shield-dot-mouse-over.json in Resources */, 4B957BF22AC7AE700062CA31 /* 01_Fire_really_small.json in Resources */, 4B957BF32AC7AE700062CA31 /* Onboarding.storyboard in Resources */, + 56CEE9102B7A72FE00CF10AA /* InfoPlist.xcstrings in Resources */, 4B957BF42AC7AE700062CA31 /* FireproofDomains.storyboard in Resources */, 4B957BF52AC7AE700062CA31 /* clickToLoadConfig.json in Resources */, 4B957BF62AC7AE700062CA31 /* Downloads.storyboard in Resources */, @@ -9048,6 +9055,7 @@ AA7EB6ED27E880B600036718 /* dark-shield-dot-mouse-over.json in Resources */, 8511E18425F82B34002F516B /* 01_Fire_really_small.json in Resources */, 85B7184A27677C2D00B4277F /* Onboarding.storyboard in Resources */, + 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */, 4B0511C3262CAA5A00F6079C /* FireproofDomains.storyboard in Resources */, EA477680272A21B700419EDA /* clickToLoadConfig.json in Resources */, B6B1E88226D5DAC30062C350 /* Downloads.storyboard in Resources */, diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 0eeb4640af..a6f5f900de 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -64,55 +64,55 @@ struct UserText { } // MARK: - Main Menu -> DuckDuckGo - static let mainMenuAppPreferences = NSLocalizedString("Preferences…", comment: "Main Menu DuckDuckGo item") - static let mainMenuAppServices = NSLocalizedString("Services", comment: "Main Menu DuckDuckGo item") - static let mainMenuAppCheckforUpdates = NSLocalizedString("Check for Updates…", comment: "Main Menu DuckDuckGo item") - static let mainMenuAppHideDuckDuckGo = NSLocalizedString("Hide DuckDuckGo", comment: "Main Menu DuckDuckGo item") - static let mainMenuAppHideOthers = NSLocalizedString("Hide Others", comment: "Main Menu DuckDuckGo item") - static let mainMenuAppShowAll = NSLocalizedString("Show All", comment: "Main Menu DuckDuckGo item") - static let mainMenuAppQuitDuckDuckGo = NSLocalizedString("Quit DuckDuckGo", comment: "Main Menu DuckDuckGo item") + static let mainMenuAppPreferences = NSLocalizedString("main-menu.app.preferences" ,value:"Preferences…", comment: "Main Menu DuckDuckGo item") + static let mainMenuAppServices = NSLocalizedString("main-menu.app.services", value:"Services", comment: "Main Menu DuckDuckGo item") + static let mainMenuAppCheckforUpdates = NSLocalizedString("main-menu.app.check-for-updates", value:"Check for Updates…", comment: "Main Menu DuckDuckGo item") + static let mainMenuAppHideDuckDuckGo = NSLocalizedString("main-menu.app.hide-duck-duck-go", value:"Hide DuckDuckGo", comment: "Main Menu DuckDuckGo item") + static let mainMenuAppHideOthers = NSLocalizedString("main-menu.app.hide-others", value:"Hide Others", comment: "Main Menu DuckDuckGo item") + static let mainMenuAppShowAll = NSLocalizedString("main-menu.app.show-all", value:"Show All", comment: "Main Menu DuckDuckGo item") + static let mainMenuAppQuitDuckDuckGo = NSLocalizedString("main-menu.app.quit-duck-duck-go", value:"Quit DuckDuckGo", comment: "Main Menu DuckDuckGo item") // MARK: - Main Menu -> -File - static let mainMenuFile = NSLocalizedString("File", comment: "Main Menu File") - static let mainMenuFileNewTab = NSLocalizedString("New Tab", comment: "Main Menu File item") - static let mainMenuFileOpenLocation = NSLocalizedString("Open Location…", comment: "Main Menu File item") - static let mainMenuFileCloseWindow = NSLocalizedString("Close Window", comment: "Main Menu File item") - static let mainMenuFileCloseAllWindows = NSLocalizedString("Close All Windows", comment: "Main Menu File item") - static let mainMenuFileSaveAs = NSLocalizedString("Save As…", comment: "Main Menu File item") - static let mainMenuFileImportBookmarksandPasswords = NSLocalizedString("Import Bookmarks and Passwords…", comment: "Main Menu File item") - static let mainMenuFileExport = NSLocalizedString("Export", comment: "Main Menu File item") - static let mainMenuFileExportPasswords = NSLocalizedString("Passwords…", comment: "Main Menu File-Export item") - static let mainMenuFileExportBookmarks = NSLocalizedString("Bookmarks…", comment: "Main Menu File-Export item") + static let mainMenuFile = NSLocalizedString("main-menu.file", value:"File", comment: "Main Menu File") + static let mainMenuFileNewTab = NSLocalizedString("main-menu.file.new-tab", value:"New Tab", comment: "Main Menu File item") + static let mainMenuFileOpenLocation = NSLocalizedString("main-menu.file.open-location", value:"Open Location…", comment: "Main Menu File item") + static let mainMenuFileCloseWindow = NSLocalizedString("main-menu.file.close-window", value:"Close Window", comment: "Main Menu File item") + static let mainMenuFileCloseAllWindows = NSLocalizedString("main-menu.file.close-all-windows", value:"Close All Windows", comment: "Main Menu File item") + static let mainMenuFileSaveAs = NSLocalizedString("main-menu.file.save-as", value:"Save As…", comment: "Main Menu File item") + static let mainMenuFileImportBookmarksandPasswords = NSLocalizedString("main-menu.file.import-bookmarks-and-passwords", value:"Import Bookmarks and Passwords…", comment: "Main Menu File item") + static let mainMenuFileExport = NSLocalizedString("main-menu.file.export", value:"Export", comment: "Main Menu File item") + static let mainMenuFileExportPasswords = NSLocalizedString("main-menu.file.export-passwords", value:"Passwords…", comment: "Main Menu File-Export item") + static let mainMenuFileExportBookmarks = NSLocalizedString("main-menu.file.export-bookmarks", value:"Bookmarks…", comment: "Main Menu File-Export item") // MARK: - Main Menu -> Edit - static let mainMenuEdit = NSLocalizedString("Edit", comment: "Main Menu Edit") - static let mainMenuEditUndo = NSLocalizedString("Undo", comment: "Main Menu Edit item") - static let mainMenuEditRedo = NSLocalizedString("Redo", comment: "Main Menu Edit item") - static let mainMenuEditCut = NSLocalizedString("Cut", comment: "Main Menu Edit item") - static let mainMenuEditCopy = NSLocalizedString("Copy", comment: "Main Menu Edit item") - static let mainMenuEditPaste = NSLocalizedString("Paste", comment: "Main Menu Edit item") - static let mainMenuEditPasteAndMatchStyle = NSLocalizedString("Paste and Match Style", comment: "Main Menu Edit item") - static let mainMenuEditDelete = NSLocalizedString("Delete", comment: "Main Menu Edit item") - static let mainMenuEditSelectAll = NSLocalizedString("Select All", comment: "Main Menu Edit item") - - static let mainMenuEditFind = NSLocalizedString("Find", comment: "Main Menu Edit item") + static let mainMenuEdit = NSLocalizedString("main-menu.edit", value:"Edit", comment: "Main Menu Edit") + static let mainMenuEditUndo = NSLocalizedString("main-menu.edit.undo", value:"Undo", comment: "Main Menu Edit item") + static let mainMenuEditRedo = NSLocalizedString("main-menu.edit.redo", value:"Redo", comment: "Main Menu Edit item") + static let mainMenuEditCut = NSLocalizedString("main-menu.edit.cut", value:"Cut", comment: "Main Menu Edit item") + static let mainMenuEditCopy = NSLocalizedString("main-menu.edit.copy", value:"Copy", comment: "Main Menu Edit item") + static let mainMenuEditPaste = NSLocalizedString("main-menu.edit.paste", value:"Paste", comment: "Main Menu Edit item") + static let mainMenuEditPasteAndMatchStyle = NSLocalizedString("main-menu.edit.paste-and-match-style", value:"Paste and Match Style", comment: "Main Menu Edit item") + static let mainMenuEditDelete = NSLocalizedString("main-menu.edit.delete", value:"Delete", comment: "Main Menu Edit item") + static let mainMenuEditSelectAll = NSLocalizedString("main-menu.edit.select-all", value:"Select All", comment: "Main Menu Edit item") + + static let mainMenuEditFind = NSLocalizedString("main-menu.edit.find", value:"Find", comment: "Main Menu Edit item") // MARK: Main Menu -> Edit -> Find - static let mainMenuEditFindFindNext = NSLocalizedString("Find Next", comment: "Main Menu Edit-Find item") - static let mainMenuEditFindFindPrevious = NSLocalizedString("Find Previous", comment: "Main Menu Edit-Find item") - static let mainMenuEditFindHideFind = NSLocalizedString("Hide Find", comment: "Main Menu Edit-Find item") + static let mainMenuEditFindFindNext = NSLocalizedString("main-menu.edit.find.find-next", value:"Find Next", comment: "Main Menu Edit-Find item") + static let mainMenuEditFindFindPrevious = NSLocalizedString("main-menu.edit.find.find-previous", value:"Find Previous", comment: "Main Menu Edit-Find item") + static let mainMenuEditFindHideFind = NSLocalizedString("main-menu.edit.find.hide-find", value:"Hide Find", comment: "Main Menu Edit-Find item") - static let mainMenuEditSpellingandGrammar = NSLocalizedString("Spelling and Grammar", comment: "Main Menu Edit item") + static let mainMenuEditSpellingandGrammar = NSLocalizedString("main-menu.edit.edit-spelling-and-grammar", value: "Spelling and Grammar", comment: "Main Menu Edit item") // MARK: Main Menu -> Edit -> Spellingand - static let mainMenuEditSpellingandShowSpellingandGrammar = NSLocalizedString("Show Spelling and Grammar", comment: "Main Menu Edit-Spellingand item") - static let mainMenuEditSpellingandCheckDocumentNow = NSLocalizedString("Check Document Now", comment: "Main Menu Edit-Spellingand item") - static let mainMenuEditSpellingandCheckSpellingWhileTyping = NSLocalizedString("Check Spelling While Typing", comment: "Main Menu Edit-Spellingand item") - static let mainMenuEditSpellingandCheckGrammarWithSpelling = NSLocalizedString("Check Grammar With Spelling", comment: "Main Menu Edit-Spellingand item") - static let mainMenuEditSpellingandCorrectSpellingAutomatically = NSLocalizedString("Correct Spelling Automatically", comment: "Main Menu Edit-Spellingand item") - - static let mainMenuEditSubstitutions = NSLocalizedString("Substitutions", comment: "Main Menu Edit item") - + static let mainMenuEditSpellingandShowSpellingandGrammar = NSLocalizedString("main-menu.edit.spelling-and.show-spelling-and-grammar", value:"Show Spelling and Grammar", comment: "Main Menu Edit-Spellingand item") + static let mainMenuEditSpellingandCheckDocumentNow = NSLocalizedString("main-menu.edit.spelling-and.check-document-now", value:"Check Document Now", comment: "Main Menu Edit-Spellingand item") + static let mainMenuEditSpellingandCheckSpellingWhileTyping = NSLocalizedString("main-menu.edit.spelling-and.check-spelling-while-typing", value:"Check Spelling While Typing", comment: "Main Menu Edit-Spellingand item") + static let mainMenuEditSpellingandCheckGrammarWithSpelling = NSLocalizedString("main-menu.edit.spelling-and.check-grammar-with-spelling", value:"Check Grammar With Spelling", comment: "Main Menu Edit-Spellingand item") + static let mainMenuEditSpellingandCorrectSpellingAutomatically = NSLocalizedString("main-menu.edit.spelling-and.correct-spelling-automatically", value:"Correct Spelling Automatically", comment: "Main Menu Edit-Spellingand item") + + static let mainMenuEditSubstitutions = NSLocalizedString("main-menu.edit.subsitutions", value:"Substitutions", comment: "Main Menu Edit item") +// TODO: Done till here // MARK: Main Menu -> Edit -> Substitutions static let mainMenuEditSubstitutionsShowSubstitutions = NSLocalizedString("Show Substitutions", comment: "Main Menu Edit-Substitutions item") static let mainMenuEditSubstitutionsSmartCopyPaste = NSLocalizedString("Smart Copy/Paste", comment: "Main Menu Edit-Substitutions item") diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 8e6122e6ba..a1b9b583bb 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -89,7 +89,7 @@ NSCameraUsageDescription Allows you to upload photographs and videos NSHumanReadableCopyright - Copyright © 2023 DuckDuckGo. All rights reserved. + Copyright © 2024 DuckDuckGo. All rights reserved. NSLocationUsageDescription Allows you to share your geolocation NSLocationWhenInUseUsageDescription diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings new file mode 100644 index 0000000000..4d7c2d94c0 --- /dev/null +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -0,0 +1,78 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo" + } + } + } + }, + "NSCameraUsageDescription" : { + "comment" : "Privacy - Camera Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Allows you to upload photographs and videos" + } + } + } + }, + "NSHumanReadableCopyright" : { + "comment" : "Copyright (human-readable)", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copyright © 2024 DuckDuckGo. All rights reserved." + } + } + } + }, + "NSLocationUsageDescription" : { + "comment" : "Privacy - Location Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Allows you to share your geolocation" + } + } + } + }, + "NSLocationWhenInUseUsageDescription" : { + "comment" : "Privacy - Location When In Use Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Allows you to share your location" + } + } + } + }, + "NSMicrophoneUsageDescription" : { + "comment" : "Privacy - Microphone Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Allows you to share recordings" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 63a3274895..fed1cec718 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -1890,7 +1890,7 @@ } }, "Copy" : { - "comment" : "Command\nMain Menu Edit item" + "comment" : "Command" }, "copy-selection" : { "comment" : "Copy selection menu item", @@ -2419,7 +2419,7 @@ } }, "Delete" : { - "comment" : "Command\nMain Menu Edit item" + "comment" : "Command" }, "delete-bookmark" : { "comment" : "Delete Bookmark button", @@ -4692,6 +4692,450 @@ "macOS version" : { "comment" : "Data import failure Report dialog description of a report field providing user‘s macOS version" }, + "main-menu.app.check-for-updates" : { + "comment" : "Main Menu DuckDuckGo item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Check for Updates…" + } + } + } + }, + "main-menu.app.hide-duck-duck-go" : { + "comment" : "Main Menu DuckDuckGo item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hide DuckDuckGo" + } + } + } + }, + "main-menu.app.hide-others" : { + "comment" : "Main Menu DuckDuckGo item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hide Others" + } + } + } + }, + "main-menu.app.preferences" : { + "comment" : "Main Menu DuckDuckGo item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Preferences…" + } + } + } + }, + "main-menu.app.quit-duck-duck-go" : { + "comment" : "Main Menu DuckDuckGo item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Quit DuckDuckGo" + } + } + } + }, + "main-menu.app.services" : { + "comment" : "Main Menu DuckDuckGo item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Services" + } + } + } + }, + "main-menu.app.show-all" : { + "comment" : "Main Menu DuckDuckGo item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show All" + } + } + } + }, + "main-menu.edit" : { + "comment" : "Main Menu Edit", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Edit" + } + } + } + }, + "main-menu.edit.copy" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copy" + } + } + } + }, + "main-menu.edit.cut" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cut" + } + } + } + }, + "main-menu.edit.delete" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Delete" + } + } + } + }, + "main-menu.edit.edit-spelling-and-grammar" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Spelling and Grammar" + } + } + } + }, + "main-menu.edit.find" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Find" + } + } + } + }, + "main-menu.edit.find.find-next" : { + "comment" : "Main Menu Edit-Find item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Find Next" + } + } + } + }, + "main-menu.edit.find.find-previous" : { + "comment" : "Main Menu Edit-Find item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Find Previous" + } + } + } + }, + "main-menu.edit.find.hide-find" : { + "comment" : "Main Menu Edit-Find item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hide Find" + } + } + } + }, + "main-menu.edit.paste" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Paste" + } + } + } + }, + "main-menu.edit.paste-and-match-style" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Paste and Match Style" + } + } + } + }, + "main-menu.edit.redo" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Redo" + } + } + } + }, + "main-menu.edit.select-all" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Select All" + } + } + } + }, + "main-menu.edit.spelling-and.check-document-now" : { + "comment" : "Main Menu Edit-Spellingand item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Check Document Now" + } + } + } + }, + "main-menu.edit.spelling-and.check-grammar-with-spelling" : { + "comment" : "Main Menu Edit-Spellingand item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Check Grammar With Spelling" + } + } + } + }, + "main-menu.edit.spelling-and.check-spelling-while-typing" : { + "comment" : "Main Menu Edit-Spellingand item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Check Spelling While Typing" + } + } + } + }, + "main-menu.edit.spelling-and.correct-spelling-automatically" : { + "comment" : "Main Menu Edit-Spellingand item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Correct Spelling Automatically" + } + } + } + }, + "main-menu.edit.spelling-and.show-spelling-and-grammar" : { + "comment" : "Main Menu Edit-Spellingand item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Spelling and Grammar" + } + } + } + }, + "main-menu.edit.subsitutions" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Substitutions" + } + } + } + }, + "main-menu.edit.undo" : { + "comment" : "Main Menu Edit item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Undo" + } + } + } + }, + "main-menu.file" : { + "comment" : "Main Menu File", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "File" + } + } + } + }, + "main-menu.file.close-all-windows" : { + "comment" : "Main Menu File item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Close All Windows" + } + } + } + }, + "main-menu.file.close-window" : { + "comment" : "Main Menu File item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Close Window" + } + } + } + }, + "main-menu.file.export" : { + "comment" : "Main Menu File item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Export" + } + } + } + }, + "main-menu.file.export-bookmarks" : { + "comment" : "Main Menu File-Export item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmarks…" + } + } + } + }, + "main-menu.file.export-passwords" : { + "comment" : "Main Menu File-Export item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Passwords…" + } + } + } + }, + "main-menu.file.import-bookmarks-and-passwords" : { + "comment" : "Main Menu File item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Import Bookmarks and Passwords…" + } + } + } + }, + "main-menu.file.new-tab" : { + "comment" : "Main Menu File item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "New Tab" + } + } + } + }, + "main-menu.file.open-location" : { + "comment" : "Main Menu File item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Location…" + } + } + } + }, + "main-menu.file.save-as" : { + "comment" : "Main Menu File item", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Save As…" + } + } + } + }, "main.menu.close.downloads" : { "comment" : "Hide Downloads Popover", "extractionState" : "extracted_with_value", diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index fd4f2c3313..b07a7136a2 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -1,10 +1,11 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "SyncUI", + defaultLocalization: "en", platforms: [ .macOS("11.4") ], products: [ .library( diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift index d173d48204..7c24341d57 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift @@ -32,9 +32,16 @@ struct SyncWithAnotherDeviceView: View { SyncDialog(spacing: 20.0) { Image("Sync-Pair-96") SyncUIViews.TextHeader(text: UserText.syncWithAnotherDeviceTitle) - Text("\(Text(UserText.syncWithAnotherDeviceSubtitle1)) \(Text(UserText.syncWithAnotherDeviceSubtitle2).bold()) \(Text(UserText.syncWithAnotherDeviceSubtitle3))") - .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(.center) + if #available(macOS 12.0, *) { + Text(syncWithAnotherDeviceInstruction) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + } else { + Text(UserText.syncWithAnotherDeviceSubtitle(syncMenuPath: UserText.syncMenuPath)) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + } + VStack(spacing: 20) { pickerView() if selectedSegment == 0 { @@ -61,6 +68,16 @@ struct SyncWithAnotherDeviceView: View { .frame(width: 420) } + @available(macOS 12, *) + private var syncWithAnotherDeviceInstruction: AttributedString { + let baseString = UserText.syncWithAnotherDeviceSubtitle(syncMenuPath: UserText.syncMenuPath) + var instructions = AttributedString(baseString) + if let range = instructions.range(of: UserText.syncMenuPath) { + instructions[range].font = .system(size: NSFont.systemFontSize, weight: .bold) + } + return instructions + } + fileprivate func pickerView() -> some View { return HStack(spacing: 0) { pickerOptionView(imageName: "QR-Icon", title: UserText.syncWithAnotherDeviceShowCodeButton, tag: 0) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings new file mode 100644 index 0000000000..abcf797258 --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings @@ -0,0 +1,1173 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "(%@)" : { + + }, + "alert.invalid-code-description" : { + "comment" : "Description for invalid code error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sorry, this code is invalid. Please make sure it was entered correctly." + } + } + } + }, + "alert.sync-error" : { + "comment" : "Title for sync error alert", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync & Backup Error" + } + } + } + }, + "alert.unable-to-create-recovery-pdf-description" : { + "comment" : "Description for unable to create recovery pdf error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unable to create the recovery PDF." + } + } + } + }, + "alert.unable-to-delete-data-description" : { + "comment" : "Description for unable to delete data error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unable to delete data on the server." + } + } + } + }, + "alert.unable-to-merge-two-accounts-description" : { + "comment" : "Description for unable to merge two accounts error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device." + } + } + } + }, + "alert.unable-to-remove-device-description" : { + "comment" : "Description for unable to remove device error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unable to remove this device from Sync & Backup." + } + } + } + }, + "alert.unable-to-sync-to-server-description" : { + "comment" : "Description for unable to sync to server error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unable to connect to the server." + } + } + } + }, + "alert.unable-to-sync-with-another-device-description" : { + "comment" : "Description for unable to sync with another device error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unable to Sync with another device." + } + } + } + }, + "alert.unable-to-turn-sync-off-description" : { + "comment" : "Description for unable to turn sync off error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unable to turn Sync & Backup off." + } + } + } + }, + "alert.unable-to-update-device-name-description" : { + "comment" : "Description for unable to update device name error", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unable to update the device name." + } + } + } + }, + "cancel" : { + "comment" : "Cancel button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cancel" + } + } + } + }, + "copy" : { + "comment" : "Copy button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copy" + } + } + } + }, + "done" : { + "comment" : "Done button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Done" + } + } + } + }, + "next" : { + "comment" : "Next button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Next" + } + } + } + }, + "notnow" : { + "comment" : "Not Now button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Not Now" + } + } + } + }, + "ok" : { + "comment" : "OK button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "OK" + } + } + } + }, + "paste" : { + "comment" : "Paste button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Paste" + } + } + } + }, + "paste-from-clipboard" : { + "comment" : "Paste button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Paste from Clipboard" + } + } + } + }, + "preferences.begin-sync.card-button" : { + "comment" : "Button text on the Begin Syncing card in sync settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync With Another Device" + } + } + } + }, + "preferences.begin-sync.card-description" : { + "comment" : "Begin Syncing card description in sync settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Securely sync bookmarks and passwords between your devices." + } + } + } + }, + "preferences.begin-sync.card-footer" : { + "comment" : "Footer / captoin on the Begin Syncing card in sync settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key." + } + } + } + }, + "preferences.begin-sync.card-title" : { + "comment" : "Begin Syncing card title in sync settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Begin Syncing" + } + } + } + }, + "preferences.enter-recovery-code.dialog-action1" : { + "comment" : "Sync enter recovery code dialog first possible action", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Paste Code Here" + } + } + } + }, + "preferences.enter-recovery-code.dialog-action2" : { + "comment" : "Sync enter recovery code dialog second possible action", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "or scan QR code with a device that is still connected" + } + } + } + }, + "preferences.enter-recovery-code.dialog-subtitle" : { + "comment" : "Sync enter recovery code dialog subtitle", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Enter the code on your Recovery PDF, or another synced device, to recover your synced data." + } + } + } + }, + "preferences.enter-recovery-code.dialog-title" : { + "comment" : "Sync enter recovery code dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Enter Code" + } + } + } + }, + "preferences.other-options.section-title" : { + "comment" : "Sync settings. Other Options section title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Other Options" + } + } + } + }, + "preferences.preparing-to-sync.dialog-action" : { + "comment" : "Sync preparing to sync dialog action", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Connecting…" + } + } + } + }, + "preferences.preparing-to-sync.dialog-subtitle" : { + "comment" : "Preparing to sync dialog subtitle during sync set up", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "We're setting up the connection to synchronize your bookmarks and saved logins with the other device." + } + } + } + }, + "preferences.preparing-to-sync.dialog-title" : { + "comment" : "Peparing to sync dialog title during sync set up", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Preparing To Sync" + } + } + } + }, + "preferences.recover-data.link-title" : { + "comment" : "Sync settings. Link to recover synced data.", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Recover Synced Data" + } + } + } + }, + "preferences.recover-synced-data.dialog-button" : { + "comment" : "Sync recover synced data dialog button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Get Started" + } + } + } + }, + "preferences.recover-synced-data.dialog-subtitle" : { + "comment" : "Recover synced data during Sync revoery process dialog subtitle", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync." + } + } + } + }, + "preferences.recover-synced-data.dialog-title" : { + "comment" : "Sync recover synced data dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Recover Synced Data" + } + } + } + }, + "preferences.sync" : { + "comment" : "Show sync preferences", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync & Backup" + } + } + } + }, + "preferences.sync-this-device.link-title" : { + "comment" : "Sync settings. Title of a link to start setting up sync and backup the device", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync and Back Up This Device" + } + } + } + }, + "preferences.sync.connected" : { + "comment" : "Sync state is enabled", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync Enabled" + } + } + } + }, + "preferences.sync.current-device-details" : { + "comment" : "Sync Settings device details button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Details..." + } + } + } + }, + "preferences.sync.remove-device" : { + "comment" : "Button to remove a device", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove..." + } + } + } + }, + "preferences.sync.remove-device-button" : { + "comment" : "Button text on remove a device confirmation button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove Device" + } + } + } + }, + "preferences.sync.remove-device-message" : { + "comment" : "Message to confirm the device will no longer be able to access the synced data - devoce name item inserted", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "\"%@\" will no longer be able to access your synced data." + } + } + } + }, + "preferences.sync.remove-device-title" : { + "comment" : "Title on remove a device confirmation", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove device?" + } + } + } + }, + "preferences.sync.rollout-banner.description" : { + "comment" : "Description of rollout banner", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices." + } + } + } + }, + "preferences.sync.sync-with-another-device.dialog-subtitle1" : { + "comment" : "Sync with another device dialog subtitle - Instruction with sync menu path item inserted", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device." + } + } + } + }, + "preferences.sync.sync-with-another-device.dialog-title" : { + "comment" : "Sync with another device dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync With Another Device" + } + } + } + }, + "preferences.sync.sync-with-another-device.enter-code-button" : { + "comment" : "Text on enter code button on Sync with another device dialog", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Enter Code" + } + } + } + }, + "preferences.sync.sync-with-another-device.enter-code-explanation" : { + "comment" : "Sync with another device dialog enter code explanation", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Paste the code here to sync." + } + } + } + }, + "preferences.sync.sync-with-another-device.show-code-button" : { + "comment" : "Text on show code button on Sync with another device dialog", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Code" + } + } + } + }, + "preferences.sync.sync-with-another-device.show-code-explanation" : { + "comment" : "Sync with another device dialog show code explanation", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Share this code to connect with a desktop machine." + } + } + } + }, + "preferences.sync.sync-with-another-device.show-qr-code-explanation" : { + "comment" : "Sync with another device dialog show qr code explanation", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Scan this QR code to connect." + } + } + } + }, + "preferences.sync.sync-with-another-device.view-qr-code-link" : { + "comment" : "Sync with another device dialog view qr code link", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "View QR Code" + } + } + } + }, + "preferences.sync.sync-with-another-device.view-text-code-link" : { + "comment" : "Sync with another device dialog view text code link", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "View Text Code" + } + } + } + }, + "preferences.sync.sync-with-server-button" : { + "comment" : "Sync with server dialog button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Turn On Sync & Backup" + } + } + } + }, + "preferences.sync.sync-with-server-subtitle1" : { + "comment" : "Sync with server dialog first subtitle", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices." + } + } + } + }, + "preferences.sync.sync-with-server-subtitle2" : { + "comment" : "Sync with server dialog second subtitle", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The encryption key is only stored on your device, DuckDuckGo cannot access it." + } + } + } + }, + "preferences.sync.sync-with-server-title" : { + "comment" : "Sync with server dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync and Back Up This Device" + } + } + } + }, + "preferences.sync.synced-devices" : { + "comment" : "Settings section title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Synced Devices" + } + } + } + }, + "preferences.sync.this-device" : { + "comment" : "Indicator of a current user's device on the list", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This Device" + } + } + } + }, + "preferences.sync.turn-off" : { + "comment" : "Turn off sync confirmation dialog button title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Turn Off" + } + } + } + }, + "preferences.sync.turn-off-and-delete-data" : { + "comment" : "Disable and delete data sync button caption", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Turn Off and Delete Server Data…" + } + } + } + }, + "preferences.sync.turn-off.confirm.message" : { + "comment" : "Turn off sync confirmation dialog message", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This device will no longer be able to access your synced data." + } + } + } + }, + "preferences.sync.turn-off.confirm.title" : { + "comment" : "Turn off sync confirmation dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Turn off sync?" + } + } + } + }, + "preferences.sync.turn-off.ellipsis" : { + "comment" : "Disable sync button caption", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Turn Off Sync…" + } + } + } + }, + "prefrences.sync.bookmarks-limit-exceeded-action" : { + "comment" : "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Manage Bookmarks" + } + } + } + }, + "prefrences.sync.bookmarks-limit-exceeded-description" : { + "comment" : "Description for sync bookmarks limits exceeded warning", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmark limit exceeded. Delete some to resume syncing." + } + } + } + }, + "prefrences.sync.credentials-limit-exceeded-action" : { + "comment" : "Button title for sync credentials limits exceeded warning to go to manage passwords", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Manage passwords…" + } + } + } + }, + "prefrences.sync.credentials-limit-exceeded-description" : { + "comment" : "Description for sync credentials limits exceeded warning", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Logins limit exceeded. Delete some to resume syncing." + } + } + } + }, + "prefrences.sync.delete-account.button" : { + "comment" : "Label for delete account button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Delete Data" + } + } + } + }, + "prefrences.sync.delete-account.message" : { + "comment" : "Message for delete account confirmation pop up", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "These devices will be disconnected and your synced data will be deleted from the server." + } + } + } + }, + "prefrences.sync.delete-account.title" : { + "comment" : "Title for delete account confirmation pop up", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Delete server data?" + } + } + } + }, + "prefrences.sync.device-details.label" : { + "comment" : "The text entry label to name the device", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Name" + } + } + } + }, + "prefrences.sync.device-details.prompt" : { + "comment" : "The text entry prompt to name the device", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Device name" + } + } + } + }, + "prefrences.sync.device-details.title" : { + "comment" : "The title of the device details dialog", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Device Details" + } + } + } + }, + "prefrences.sync.device-synced" : { + "comment" : "Sync setup confirmation dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your data is synced!" + } + } + } + }, + "prefrences.sync.fetch-favicons-onboarding-message" : { + "comment" : "Text for fetch favicons onboarding dialog", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced." + } + } + } + }, + "prefrences.sync.fetch-favicons-onboarding-title" : { + "comment" : "Title for fetch favicons onboarding dialog", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Download Missing Icons?" + } + } + } + }, + "prefrences.sync.fetch-favicons-option-caption" : { + "comment" : "Caption for fetch favicons option", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network." + } + } + } + }, + "prefrences.sync.fetch-favicons-option-title" : { + "comment" : "Title for fetch favicons option", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Auto-Download Icons" + } + } + } + }, + "prefrences.sync.keep-favicons-updated" : { + "comment" : "Title of the confirmation button for favicons fetching", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep Bookmarks Icons Updated" + } + } + } + }, + "prefrences.sync.limit-exceeded-title" : { + "comment" : "Title for sync limits exceeded warning", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync Paused" + } + } + } + }, + "prefrences.sync.options-section-title" : { + "comment" : "Title for options settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Options" + } + } + } + }, + "prefrences.sync.recovery" : { + "comment" : "Sync settings section title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Recovery" + } + } + } + }, + "prefrences.sync.recovery-instructions" : { + "comment" : "Instructions on how to restore synced data", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "If you lose your device, you will need this recovery code to restore your synced data." + } + } + } + }, + "prefrences.sync.recovery-pdf-copy-code-button" : { + "comment" : "Sync recovery PDF copy code button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copy Code" + } + } + } + }, + "prefrences.sync.recovery-pdf-explanation" : { + "comment" : "Sync recovery PDF explanation", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF." + } + } + } + }, + "prefrences.sync.recovery-pdf-save-pdf-button" : { + "comment" : "Sync recovery PDF save pdf button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Save PDF" + } + } + } + }, + "prefrences.sync.recovery-pdf-warning" : { + "comment" : "Sync recovery PDF warning", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Anyone with access to this code can access your synced data, so please keep it in a safe place." + } + } + } + }, + "prefrences.sync.save-recovery-pdf" : { + "comment" : "Caption for a button to save Sync recovery PDF", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Save Your Recovery Code" + } + } + } + }, + "prefrences.sync.share-favorite-option-caption" : { + "comment" : "Caption for share favorite option", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate." + } + } + } + }, + "prefrences.sync.share-favorite-option-title" : { + "comment" : "Title for share favorite option", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unify Favorites Across Devices" + } + } + } + }, + "share" : { + "comment" : "Share button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Share" + } + } + } + }, + "submit" : { + "comment" : "Submit button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Submit" + } + } + } + }, + "sync.menu.path" : { + "comment" : "Sync Menu Path", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Settings › Sync & Backup" + } + } + } + }, + "sync.warning.data-syncing-disabled-upgrade-required" : { + "comment" : "Data syncing unavailable warning message", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue." + } + } + } + }, + "sync.warning.sync-paused" : { + "comment" : "Title of the warning message that Sync & Backup is Paused", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync & Backup is Paused" + } + } + } + }, + "sync.warning.sync-unavailable" : { + "comment" : "Title of the warning message that sync and backup are unavailable", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync & Backup is Unavailable" + } + } + } + }, + "sync.warning.sync-unavailable-message" : { + "comment" : "Data syncing unavailable warning message", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sorry, but Sync & Backup is currently unavailable. Please try again later." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index 4864293d68..ca0babbe0f 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -34,18 +34,18 @@ enum UserText { // Sync Set Up View // Begin Sync card - static let beginSyncTitle = NSLocalizedString("preferences.begin.sync-card-title", value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") - static let beginSyncDescription = NSLocalizedString("preferences.begin.sync-card-description", value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") - static let beginSyncButton = NSLocalizedString("preferences.begin.sync-card-button", value: "Sync With Another Device", comment: "Begin Syncing card button in sync settings") - static let beginSyncFooter = NSLocalizedString("preferences.begin.sync-card-footer", value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Begin Syncing card footer in sync settings") + static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") + static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") + static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") + static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / captoin on the Begin Syncing card in sync settings") // Options - static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", value: "Other Options", comment: "Sync preferences other options section title") - static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", value: "Sync and Back Up This Device", comment: "Sync preferences sync this device link title") - static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", value: "Recover Synced Data", comment: "Sync preferences recover data link title") + static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", value: "Other Options", comment: "Sync settings. Other Options section title") + static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") + static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") // Preparing to sync dialog - static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", value: "Preparing To Sync", comment: "Sync preparing to sync dialog title") - static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Sync preparing to sync dialog subtitle") + static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", value: "Preparing To Sync", comment: "Peparing to sync dialog title during sync set up") + static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", value: "Connecting…", comment: "Sync preparing to sync dialog action") // Enter recovery code dialog @@ -56,7 +56,7 @@ enum UserText { // Recover synced data dialog static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", value: "Recover Synced Data", comment: "Sync recover synced data dialog title") - static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Sync recover synced data dialog subtitle") + static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync revoery process dialog subtitle") static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", value: "Get Started", comment: "Sync recover synced data dialog button") // Sync Title @@ -73,7 +73,7 @@ enum UserText { // Delete server data static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") // sync connected - static let syncConnected = NSLocalizedString("preferences.sync.connected", value: "Sync Enabled", comment: "Sync state") + static let syncConnected = NSLocalizedString("preferences.sync.connected", value: "Sync Enabled", comment: "Sync state is enabled") // synced devices static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", value: "Synced Devices", comment: "Settings section title") static let thisDevice = NSLocalizedString("preferences.sync.this-device", value: "This Device", comment: "Indicator of a current user's device on the list") @@ -82,11 +82,11 @@ enum UserText { // Remove device dialog static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", value: "Remove device?", comment: "Title on remove a device confirmation") - static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", value: "Remove Device", comment: "Button to on remove a device confirmation") + static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", value: "Remove Device", comment: "Button text on remove a device confirmation button") static func removeDeviceConfirmMessage(_ deviceName: String) -> String { let localized = NSLocalizedString("preferences.sync.remove-device-message", value: "\"%@\" will no longer be able to access your synced data.", - comment: "") + comment: "Message to confirm the device will no longer be able to access the synced data - devoce name item inserted") return String(format: localized, deviceName) } @@ -95,11 +95,13 @@ enum UserText { // Sync with another device dialog static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", value: "Sync With Another Device", comment: "Sync with another device dialog title") - static let syncWithAnotherDeviceSubtitle1 = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", value: "Go to ", comment: "Sync with another device dialog subtitle first part") - static let syncWithAnotherDeviceSubtitle2 = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle2", value: "Settings › Sync ", comment: "Sync with another device dialog subtitle second part") - static let syncWithAnotherDeviceSubtitle3 = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle3", value: "in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle thisrd part") - static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", value: "Show Code", comment: "Sync with another device dialog show code button") - static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", value: "Enter Code", comment: "Sync with another device dialog enter code button") + static func syncWithAnotherDeviceSubtitle(syncMenuPath: String) -> String { + let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") + return String(format: localized, syncMenuPath) + } + static let syncMenuPath = NSLocalizedString("sync.menu.path", value: "Settings › Sync & Backup", comment: "Sync Menu Path") + static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", value: "Show Code", comment: "Text on show code button on Sync with another device dialog") + static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") @@ -120,16 +122,16 @@ enum UserText { static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", value: "Turn On Sync & Backup", comment: "Sync with server dialog button") // Device synced dialog - static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", value: "Your data is synced!", comment: "Sync setup dialog title") + static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", value: "Your data is synced!", comment: "Sync setup confirmation dialog title") // Device details static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", value: "Device Details", comment: "The title of the device details dialog") - static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", value: "Name", comment: "The text entry label") - static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", value: "Device name", comment: "The text entry prompt") + static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", value: "Name", comment: "The text entry label to name the device") + static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", value: "Device name", comment: "The text entry prompt to name the device") // Delete Account Dialog - static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", value: "Delete server data?", comment: "Title for delete account") - static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account") + static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", value: "Delete server data?", comment: "Title for delete account confirmation pop up") + static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", value: "Delete Data", comment: "Label for delete account button") // Sync enabled options @@ -143,8 +145,8 @@ enum UserText { static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", value: "Sync Paused", comment: "Title for sync limits exceeded warning") static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Description for sync bookmarks limits exceeded warning") static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") - static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to manage bookmarks") - static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords...", comment: "Button title for sync credentials limits exceeded warning to manage logins") + static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync & Backup Error", comment: "Title for sync error alert") static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") @@ -161,8 +163,8 @@ enum UserText { static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") // Sync Feature Flags - static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message") - static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") + static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") } From bfe1ffcb2278f7574af8c6daccbf3204af3fdaac Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Tue, 13 Feb 2024 14:47:52 +0000 Subject: [PATCH 2/4] After Burning, Only Launch a New Window if the App is Active (#2195) Task/Issue URL: https://app.asana.com/0/1177771139624306/1205135252111373/f **Description**: From app feedback: "When I clear my history and switch to another app, I don't expect the browser to suddenly retake focus because of its clearing history animation." --- DuckDuckGo/Fire/Model/Fire.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 7fa41f8131..f67d4072e4 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -300,6 +300,9 @@ final class Fire { } } + // If the app is not active, don't retake focus by opening a new window + guard NSApp.isActive else { return } + // Open a new window in case there is none DispatchQueue.main.async { [weak self] in guard let self else { return } From 12ffc6f94deb0cb87131a5bdc5a235ed34ed0e7f Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:08:34 +0100 Subject: [PATCH 3/4] Positioning of the preview fixed for external display (#2200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1206591283698064/f **Description**: This PR fixes an issue with previews being at the wrong position on external display when the main MacBook display is set as main Screenshot 2024-02-13 at 2 14 47 PM --- DuckDuckGo/TabPreview/TabPreviewWindowController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift index 681a6da639..6e4e5f4456 100644 --- a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift @@ -129,7 +129,8 @@ final class TabPreviewWindowController: NSWindowController { // Make sure preview is presented within screen if let screenVisibleFrame = window.screen?.visibleFrame { - topLeftPoint.x = min(topLeftPoint.x, screenVisibleFrame.width - window.frame.width) + topLeftPoint.x = min(topLeftPoint.x, screenVisibleFrame.origin.x + screenVisibleFrame.width - window.frame.width) + topLeftPoint.x = max(topLeftPoint.x, screenVisibleFrame.origin.x) let windowHeight = window.frame.size.height if topLeftPoint.y <= windowHeight + screenVisibleFrame.origin.y { From 531c0e556bf1cb7b12d4f2567faf7057676e7a71 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 14 Feb 2024 15:26:04 +0100 Subject: [PATCH 4/4] Move Subscription package into BSK (#2175) Task/Issue URL: https://app.asana.com/0/0/1206548012263572/f **Description**: Move `Subscription` package into BSK --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 13 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../DataBrokerProtection/Package.swift | 2 +- LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/PixelKit/Package.swift | 2 +- LocalPackages/Subscription/.gitignore | 8 - LocalPackages/Subscription/Package.swift | 34 --- .../Sources/Subscription/AccountManager.swift | 250 ----------------- .../AccountKeychainStorage.swift | 195 ------------- .../AccountStorage/AccountStorage.swift | 31 --- .../AppStoreAccountManagementFlow.swift | 58 ---- .../Flows/AppStore/AppStorePurchaseFlow.swift | 113 -------- .../Flows/AppStore/AppStoreRestoreFlow.swift | 99 ------- .../Subscription/Flows/PurchaseFlow.swift | 68 ----- .../Flows/Stripe/StripePurchaseFlow.swift | 96 ------- .../NSNotificationName+Subscription.swift | 30 -- .../Subscription/PurchaseManager.swift | 260 ------------------ .../Subscription/Services/APIService.swift | 113 -------- .../Subscription/Services/AuthService.swift | 111 -------- .../Services/SubscriptionService.swift | 96 ------- .../SubscriptionPurchaseEnvironment.swift | 65 ----- .../Subscription/URL+Subscription.swift | 59 ---- .../SubscriptionTests/SubscriptionTests.swift | 23 -- LocalPackages/SubscriptionUI/Package.swift | 4 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- 29 files changed, 18 insertions(+), 1728 deletions(-) delete mode 100644 LocalPackages/Subscription/.gitignore delete mode 100644 LocalPackages/Subscription/Package.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/AccountManager.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountKeychainStorage.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountStorage.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift delete mode 100644 LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift delete mode 100644 LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1729d7b028..51e2f0b5c3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -104,13 +104,13 @@ 1E0068AD2B1673BB00BBF43B /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 1E0068AC2B1673BB00BBF43B /* SubscriptionUI */; }; 1E0C72062ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; 1E0C72072ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; + 1E21F8E32B73E48600FB272E /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1E21F8E22B73E48600FB272E /* Subscription */; }; 1E2AE4C72ACB215900684E0A /* NetworkProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */; }; 1E2AE4C82ACB216B00684E0A /* HoverTrackingArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B140872ABDBCC1004F8E85 /* HoverTrackingArea.swift */; }; 1E2AE4CA2ACB21A000684E0A /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; 1E2AE4CB2ACB21C800684E0A /* HardwareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9579202AC687170062CA31 /* HardwareModel.swift */; }; 1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */; }; 1E7E2E942902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */; }; - 1E934E2B2B167CA80084722B /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1E934E2A2B167CA80084722B /* Subscription */; }; 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E3E2912A10D0051A99B /* ContentBlocking */; }; 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E402912A10D0051A99B /* PrivacyDashboard */; }; 1E950E432912A10D0051A99B /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E422912A10D0051A99B /* UserScript */; }; @@ -3322,7 +3322,6 @@ 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlockingRulesUpdateObserver.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 1E862A882A9FC01200F84D4B /* SubscriptionUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SubscriptionUI; sourceTree = ""; }; - 1E8F997E2B221B3600AC5D34 /* Subscription */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Subscription; sourceTree = ""; }; 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityTheftRestorationPagesUserScript.swift; sourceTree = ""; }; 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; @@ -4556,7 +4555,6 @@ buildActionMask = 2147483647; files = ( 4B957BD52AC7AE700062CA31 /* QuickLookUI.framework in Frameworks */, - 1E934E2B2B167CA80084722B /* Subscription in Frameworks */, 3143C8792B0D1F3D00382627 /* DataBrokerProtection in Frameworks */, 372217842B33380E00B8E9C2 /* TestUtils in Frameworks */, 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */, @@ -4573,6 +4571,7 @@ 4B957BE22AC7AE700062CA31 /* Sparkle in Frameworks */, 373FB4B52B4D6C57004C88D6 /* PreferencesViews in Frameworks */, 4B957BE32AC7AE700062CA31 /* Navigation in Frameworks */, + 1E21F8E32B73E48600FB272E /* Subscription in Frameworks */, 4B957BE42AC7AE700062CA31 /* DDGSync in Frameworks */, 4B957BE52AC7AE700062CA31 /* OpenSSL in Frameworks */, 4B957BE62AC7AE700062CA31 /* PrivacyDashboard in Frameworks */, @@ -5069,7 +5068,6 @@ 4BE15DB12A0B0DD500898243 /* PixelKit */, 378F44E229B4B7B600899924 /* SwiftUIExtensions */, 37BA812B29B3CB8A0053F1A3 /* SyncUI */, - 1E8F997E2B221B3600AC5D34 /* Subscription */, 1E862A882A9FC01200F84D4B /* SubscriptionUI */, 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */, 7B76E6852AD5D77600186A84 /* XPCHelper */, @@ -8523,10 +8521,10 @@ 7B31FD8F2AD1257B0086AA24 /* NetworkProtectionIPC */, 3143C8782B0D1F3D00382627 /* DataBrokerProtection */, 1E0068AC2B1673BB00BBF43B /* SubscriptionUI */, - 1E934E2A2B167CA80084722B /* Subscription */, 37269F022B332FD8005E8E46 /* Common */, 372217832B33380E00B8E9C2 /* TestUtils */, 373FB4B42B4D6C57004C88D6 /* PreferencesViews */, + 1E21F8E22B73E48600FB272E /* Subscription */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -13355,7 +13353,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 106.0.1; + version = 107.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -13397,8 +13395,9 @@ isa = XCSwiftPackageProductDependency; productName = SubscriptionUI; }; - 1E934E2A2B167CA80084722B /* Subscription */ = { + 1E21F8E22B73E48600FB272E /* Subscription */ = { isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Subscription; }; 1E950E3E2912A10D0051A99B /* ContentBlocking */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d9f3b63ccb..954bc34c8c 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "3f5e33ec3d75dd2c130cfc6915c0a6e8efeb96f1", - "version" : "106.0.1" + "revision" : "328ce451fd1593809d1470ab5a0b5242a595f88c", + "version" : "107.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 312464c0b6..f1681bc73f 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index b2d8f701a5..cfd61b26c4 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index c312b1bd53..ee441bef03 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index ef9e6cdfca..b0824cf51b 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), ], targets: [ .target( diff --git a/LocalPackages/Subscription/.gitignore b/LocalPackages/Subscription/.gitignore deleted file mode 100644 index 0023a53406..0000000000 --- a/LocalPackages/Subscription/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/LocalPackages/Subscription/Package.swift b/LocalPackages/Subscription/Package.swift deleted file mode 100644 index 0801d55691..0000000000 --- a/LocalPackages/Subscription/Package.swift +++ /dev/null @@ -1,34 +0,0 @@ -// swift-tools-version: 5.8 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "Subscription", - platforms: [ .macOS("11.4") ], - products: [ - .library( - name: "Subscription", - targets: ["Subscription"]), - ], - dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), - ], - targets: [ - .target( - name: "Subscription", - dependencies: [ - .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), - ], - swiftSettings: [ - .define("DEBUG", .when(configuration: .debug)) - ], - plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] - ), - .testTarget( - name: "SubscriptionTests", - dependencies: ["Subscription"], - plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] - ), - ] -) diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift b/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift deleted file mode 100644 index e234708385..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/AccountManager.swift +++ /dev/null @@ -1,250 +0,0 @@ -// -// AccountManager.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -public extension Notification.Name { - static let accountDidSignIn = Notification.Name("com.duckduckgo.subscription.AccountDidSignIn") - static let accountDidSignOut = Notification.Name("com.duckduckgo.subscription.AccountDidSignOut") -} - -public protocol AccountManagerKeychainAccessDelegate: AnyObject { - func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) -} - -public protocol AccountManaging { - - var accessToken: String? { get } - -} - -public class AccountManager: AccountManaging { - - private let storage: AccountStorage - public weak var delegate: AccountManagerKeychainAccessDelegate? - - public var isUserAuthenticated: Bool { - return accessToken != nil - } - - public init(storage: AccountStorage = AccountKeychainStorage()) { - self.storage = storage - } - - public var authToken: String? { - do { - return try storage.getAuthToken() - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getAuthToken, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - - return nil - } - } - - public var accessToken: String? { - do { - return try storage.getAccessToken() - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getAccessToken, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - - return nil - } - } - - public var email: String? { - do { - return try storage.getEmail() - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getEmail, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - - return nil - } - } - - public var externalID: String? { - do { - return try storage.getExternalID() - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .getExternalID, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - - return nil - } - } - - public func storeAuthToken(token: String) { - os_log(.info, log: .subscription, "[AccountManager] storeAuthToken") - - do { - try storage.store(authToken: token) - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .storeAuthToken, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - } - } - - public func storeAccount(token: String, email: String?, externalID: String?) { - os_log(.info, log: .subscription, "[AccountManager] storeAccount") - - do { - try storage.store(accessToken: token) - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .storeAccessToken, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - } - - do { - try storage.store(email: email) - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .storeEmail, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - } - - do { - try storage.store(externalID: externalID) - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .storeExternalID, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - } - NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil) - } - - public func signOut() { - os_log(.info, log: .subscription, "[AccountManager] signOut") - - do { - try storage.clearAuthenticationState() - } catch { - if let error = error as? AccountKeychainAccessError { - delegate?.accountManagerKeychainAccessFailed(accessType: .clearAuthenticationData, error: error) - } else { - assertionFailure("Expected AccountKeychainAccessError") - } - } - - NotificationCenter.default.post(name: .accountDidSignOut, object: self, userInfo: nil) - } - - // MARK: - - - public func hasEntitlement(for name: String) async -> Bool { - await fetchEntitlements().contains(name) - } - - public func fetchEntitlements() async -> [String] { - guard let accessToken else { return [] } - - switch await AuthService.validateToken(accessToken: accessToken) { - case .success(let response): - let entitlements = response.account.entitlements - return entitlements.map { $0.name } - - case .failure(let error): - os_log(.error, log: .subscription, "[AccountManager] fetchEntitlements error: %{public}@", error.localizedDescription) - return [] - } - } - - public func exchangeAuthTokenToAccessToken(_ authToken: String) async -> Result { - switch await AuthService.getAccessToken(token: authToken) { - case .success(let response): - return .success(response.accessToken) - case .failure(let error): - os_log(.error, log: .subscription, "[AccountManager] exchangeAuthTokenToAccessToken error: %{public}@", error.localizedDescription) - return .failure(error) - } - } - - public typealias AccountDetails = (email: String?, externalID: String) - - public func fetchAccountDetails(with accessToken: String) async -> Result { - switch await AuthService.validateToken(accessToken: accessToken) { - case .success(let response): - return .success(AccountDetails(email: response.account.email, externalID: response.account.externalID)) - case .failure(let error): - os_log(.error, log: .subscription, "[AccountManager] fetchAccountDetails error: %{public}@", error.localizedDescription) - return .failure(error) - } - } - - public func checkSubscriptionState() async { - os_log(.info, log: .subscription, "[AccountManager] checkSubscriptionState") - - guard let token = accessToken else { return } - - if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { - if !response.isSubscriptionActive { - signOut() - } - } - } - - @discardableResult - public static func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { - var count = 0 - var hasEntitlements = false - - repeat { - hasEntitlements = await !AccountManager().fetchEntitlements().isEmpty - - if hasEntitlements { - break - } else { - count += 1 - try? await Task.sleep(seconds: waitTime) - } - } while !hasEntitlements && count < retryCount - - return hasEntitlements - } -} - -extension Task where Success == Never, Failure == Never { - static func sleep(seconds: Double) async throws { - let duration = UInt64(seconds * 1_000_000_000) - try await Task.sleep(nanoseconds: duration) - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountKeychainStorage.swift b/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountKeychainStorage.swift deleted file mode 100644 index 19596f764e..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountKeychainStorage.swift +++ /dev/null @@ -1,195 +0,0 @@ -// -// AccountKeychainStorage.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -public enum AccountKeychainAccessType: String { - case getAuthToken - case storeAuthToken - case getAccessToken - case storeAccessToken - case getEmail - case storeEmail - case getExternalID - case storeExternalID - case clearAuthenticationData -} - -public enum AccountKeychainAccessError: Error, Equatable { - case failedToDecodeKeychainValueAsData - case failedToDecodeKeychainDataAsString - case keychainSaveFailure(OSStatus) - case keychainDeleteFailure(OSStatus) - case keychainLookupFailure(OSStatus) - - public var errorDescription: String { - switch self { - case .failedToDecodeKeychainValueAsData: return "failedToDecodeKeychainValueAsData" - case .failedToDecodeKeychainDataAsString: return "failedToDecodeKeychainDataAsString" - case .keychainSaveFailure: return "keychainSaveFailure" - case .keychainDeleteFailure: return "keychainDeleteFailure" - case .keychainLookupFailure: return "keychainLookupFailure" - } - } -} - -public class AccountKeychainStorage: AccountStorage { - - public init() {} - - public func getAuthToken() throws -> String? { - try Self.getString(forField: .authToken) - } - - public func store(authToken: String) throws { - try Self.set(string: authToken, forField: .authToken) - } - - public func getAccessToken() throws -> String? { - try Self.getString(forField: .accessToken) - } - - public func store(accessToken: String) throws { - try Self.set(string: accessToken, forField: .accessToken) - } - - public func getEmail() throws -> String? { - try Self.getString(forField: .email) - } - - public func getExternalID() throws -> String? { - try Self.getString(forField: .externalID) - } - - public func store(externalID: String?) throws { - if let externalID = externalID, !externalID.isEmpty { - try Self.set(string: externalID, forField: .externalID) - } else { - try Self.deleteItem(forField: .externalID) - } - } - - public func store(email: String?) throws { - if let email = email, !email.isEmpty { - try Self.set(string: email, forField: .email) - } else { - try Self.deleteItem(forField: .email) - } - } - - public func clearAuthenticationState() throws { - try Self.deleteItem(forField: .authToken) - try Self.deleteItem(forField: .accessToken) - try Self.deleteItem(forField: .email) - try Self.deleteItem(forField: .externalID) - } - -} - -private extension AccountKeychainStorage { - - /* - Uses just kSecAttrService as the primary key, since we don't want to store - multiple accounts/tokens at the same time - */ - enum AccountKeychainField: String, CaseIterable { - case authToken = "account.authToken" - case accessToken = "account.accessToken" - case email = "account.email" - case externalID = "account.external_id" - - var keyValue: String { - (Bundle.main.bundleIdentifier ?? "com.duckduckgo") + "." + rawValue - } - } - - static func getString(forField field: AccountKeychainField) throws -> String? { - guard let data = try retrieveData(forField: field) else { - return nil - } - - if let decodedString = String(data: data, encoding: String.Encoding.utf8) { - return decodedString - } else { - throw AccountKeychainAccessError.failedToDecodeKeychainDataAsString - } - } - - static func retrieveData(forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws -> Data? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecAttrService as String: field.keyValue, - kSecReturnData as String: true, - kSecUseDataProtectionKeychain as String: useDataProtectionKeychain - ] - - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) - - if status == errSecSuccess { - if let existingItem = item as? Data { - return existingItem - } else { - throw AccountKeychainAccessError.failedToDecodeKeychainValueAsData - } - } else if status == errSecItemNotFound { - return nil - } else { - throw AccountKeychainAccessError.keychainLookupFailure(status) - } - } - - static func set(string: String, forField field: AccountKeychainField) throws { - guard let stringData = string.data(using: .utf8) else { - return - } - - try deleteItem(forField: field) - try store(data: stringData, forField: field) - } - - static func store(data: Data, forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws { - let query = [ - kSecClass: kSecClassGenericPassword, - kSecAttrSynchronizable: false, - kSecAttrService: field.keyValue, - kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock, - kSecValueData: data, - kSecUseDataProtectionKeychain: useDataProtectionKeychain] as [String: Any] - - let status = SecItemAdd(query as CFDictionary, nil) - - if status != errSecSuccess { - throw AccountKeychainAccessError.keychainSaveFailure(status) - } - } - - static func deleteItem(forField field: AccountKeychainField, useDataProtectionKeychain: Bool = true) throws { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: field.keyValue, - kSecUseDataProtectionKeychain as String: useDataProtectionKeychain] - - let status = SecItemDelete(query as CFDictionary) - - if status != errSecSuccess && status != errSecItemNotFound { - throw AccountKeychainAccessError.keychainDeleteFailure(status) - } - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountStorage.swift b/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountStorage.swift deleted file mode 100644 index 06b5e05cb5..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/AccountStorage/AccountStorage.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// AccountStorage.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -public protocol AccountStorage: AnyObject { - func getAuthToken() throws -> String? - func store(authToken: String) throws - func getAccessToken() throws -> String? - func store(accessToken: String) throws - func getEmail() throws -> String? - func store(email: String?) throws - func getExternalID() throws -> String? - func store(externalID: String?) throws - func clearAuthenticationState() throws -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift deleted file mode 100644 index 7b502d2147..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreAccountManagementFlow.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// AppStoreAccountManagementFlow.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import StoreKit -import Common - -@available(macOS 12.0, iOS 15.0, *) -public final class AppStoreAccountManagementFlow { - - public enum Error: Swift.Error { - case noPastTransaction - case authenticatingWithTransactionFailed - } - - @discardableResult - public static func refreshAuthTokenIfNeeded() async -> Result { - os_log(.info, log: .subscription, "[AppStoreAccountManagementFlow] refreshAuthTokenIfNeeded") - - var authToken = AccountManager().authToken ?? "" - - // Check if auth token if still valid - if case let .failure(validateTokenError) = await AuthService.validateToken(accessToken: authToken) { - os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] validateToken error: %{public}s", String(reflecting: validateTokenError)) - - // In case of invalid token attempt store based authentication to obtain a new one - guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return .failure(.noPastTransaction) } - - switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { - case .success(let response): - if response.externalID == AccountManager().externalID { - authToken = response.authToken - AccountManager().storeAuthToken(token: authToken) - } - case .failure(let storeLoginError): - os_log(.error, log: .subscription, "[AppStoreAccountManagementFlow] storeLogin error: %{public}s", String(reflecting: storeLoginError)) - return .failure(.authenticatingWithTransactionFailed) - } - } - - return .success(authToken) - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift deleted file mode 100644 index ef822341b0..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStorePurchaseFlow.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// AppStorePurchaseFlow.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import StoreKit -import Common - -@available(macOS 12.0, iOS 15.0, *) -public final class AppStorePurchaseFlow { - - public enum Error: Swift.Error { - case noProductsFound - case activeSubscriptionAlreadyPresent - case authenticatingWithTransactionFailed - case accountCreationFailed - case purchaseFailed - case missingEntitlements - } - - public static func subscriptionOptions() async -> Result { - os_log(.info, log: .subscription, "[AppStorePurchaseFlow] subscriptionOptions") - - let products = PurchaseManager.shared.availableProducts - - let monthly = products.first(where: { $0.id.contains("1month") }) - let yearly = products.first(where: { $0.id.contains("1year") }) - - guard let monthly, let yearly else { - os_log(.error, log: .subscription, "[AppStorePurchaseFlow] Error: noProductsFound") - return .failure(.noProductsFound) - } - - let options = [SubscriptionOption(id: monthly.id, cost: .init(displayPrice: monthly.displayPrice, recurrence: "monthly")), - SubscriptionOption(id: yearly.id, cost: .init(displayPrice: yearly.displayPrice, recurrence: "yearly"))] - - let features = SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) } - - return .success(SubscriptionOptions(platform: SubscriptionPlatformName.macos.rawValue, - options: options, - features: features)) - } - - public static func purchaseSubscription(with subscriptionIdentifier: String, emailAccessToken: String?) async -> Result { - os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription") - - let accountManager = AccountManager() - let externalID: String - - // Check for past transactions most recent - switch await AppStoreRestoreFlow.restoreAccountFromPastPurchase() { - case .success: - os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> restoreAccountFromPastPurchase: activeSubscriptionAlreadyPresent") - return .failure(.activeSubscriptionAlreadyPresent) - case .failure(let error): - os_log(.info, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription -> restoreAccountFromPastPurchase: %{public}s", String(reflecting: error)) - switch error { - case .subscriptionExpired(let expiredAccountDetails): - externalID = expiredAccountDetails.externalID - accountManager.storeAuthToken(token: expiredAccountDetails.authToken) - accountManager.storeAccount(token: expiredAccountDetails.accessToken, email: expiredAccountDetails.email, externalID: expiredAccountDetails.externalID) - default: - // No history, create new account - switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { - case .success(let response): - externalID = response.externalID - - if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(response.authToken), - case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { - accountManager.storeAuthToken(token: response.authToken) - accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) - } - case .failure(let error): - os_log(.error, log: .subscription, "[AppStorePurchaseFlow] createAccount error: %{public}s", String(reflecting: error)) - return .failure(.accountCreationFailed) - } - } - } - - // Make the purchase - switch await PurchaseManager.shared.purchaseSubscription(with: subscriptionIdentifier, externalID: externalID) { - case .success: - return .success(()) - case .failure(let error): - os_log(.error, log: .subscription, "[AppStorePurchaseFlow] purchaseSubscription error: %{public}s", String(reflecting: error)) - AccountManager().signOut() - return .failure(.purchaseFailed) - } - } - - @discardableResult - public static func completeSubscriptionPurchase() async -> Result { - os_log(.info, log: .subscription, "[AppStorePurchaseFlow] completeSubscriptionPurchase") - - let result = await AccountManager.checkForEntitlements(wait: 2.0, retry: 20) - - return result ? .success(PurchaseUpdate(type: "completed")) : .failure(.missingEntitlements) - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift deleted file mode 100644 index c0ee4df79c..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/AppStore/AppStoreRestoreFlow.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// AppStoreRestoreFlow.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import StoreKit -import Common - -@available(macOS 12.0, iOS 15.0, *) -public final class AppStoreRestoreFlow { - - public typealias RestoredAccountDetails = (authToken: String, accessToken: String, externalID: String, email: String?) - - public enum Error: Swift.Error { - case missingAccountOrTransactions - case pastTransactionAuthenticationError - case failedToObtainAccessToken - case failedToFetchAccountDetails - case failedToFetchSubscriptionDetails - case subscriptionExpired(accountDetails: RestoredAccountDetails) - } - - public static func restoreAccountFromPastPurchase() async -> Result { - os_log(.info, log: .subscription, "[AppStoreRestoreFlow] restoreAccountFromPastPurchase") - - guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { - os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: missingAccountOrTransactions") - return .failure(.missingAccountOrTransactions) - } - - let accountManager = AccountManager() - - // Do the store login to get short-lived token - let authToken: String - - switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { - case .success(let response): - authToken = response.authToken - case .failure: - os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: pastTransactionAuthenticationError") - return .failure(.pastTransactionAuthenticationError) - } - - let accessToken: String - let email: String? - let externalID: String - - switch await accountManager.exchangeAuthTokenToAccessToken(authToken) { - case .success(let exchangedAccessToken): - accessToken = exchangedAccessToken - case .failure: - os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: failedToObtainAccessToken") - return .failure(.failedToObtainAccessToken) - } - - switch await accountManager.fetchAccountDetails(with: accessToken) { - case .success(let accountDetails): - email = accountDetails.email - externalID = accountDetails.externalID - case .failure: - os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: failedToFetchAccountDetails") - return .failure(.failedToFetchAccountDetails) - } - - var isSubscriptionActive = false - - switch await SubscriptionService.getSubscriptionDetails(token: accessToken) { - case .success(let response): - isSubscriptionActive = response.isSubscriptionActive - case .failure: - os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: failedToFetchSubscriptionDetails") - return .failure(.failedToFetchSubscriptionDetails) - } - - if isSubscriptionActive { - accountManager.storeAuthToken(token: authToken) - accountManager.storeAccount(token: accessToken, email: email, externalID: externalID) - return .success(()) - } else { - let details = RestoredAccountDetails(authToken: authToken, accessToken: accessToken, externalID: externalID, email: email) - os_log(.error, log: .subscription, "[AppStoreRestoreFlow] Error: subscriptionExpired") - return .failure(.subscriptionExpired(accountDetails: details)) - } - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift deleted file mode 100644 index f32765ff96..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/PurchaseFlow.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// PurchaseFlow.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -public struct SubscriptionOptions: Encodable { - let platform: String - let options: [SubscriptionOption] - let features: [SubscriptionFeature] -} - -public struct SubscriptionOption: Encodable { - let id: String - let cost: SubscriptionOptionCost -} - -struct SubscriptionOptionCost: Encodable { - let displayPrice: String - let recurrence: String -} - -public struct SubscriptionFeature: Encodable { - let name: String -} - -// MARK: - - -public enum SubscriptionFeatureName: String, CaseIterable { - case privateBrowsing = "private-browsing" - case privateSearch = "private-search" - case emailProtection = "email-protection" - case appTrackingProtection = "app-tracking-protection" - case vpn = "vpn" - case personalInformationRemoval = "personal-information-removal" - case identityTheftRestoration = "identity-theft-restoration" -} - -public enum SubscriptionPlatformName: String { - case macos - case stripe -} - -// MARK: - - -public struct PurchaseUpdate: Codable { - let type: String - let token: String? - - public init(type: String, token: String? = nil) { - self.type = type - self.token = token - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift b/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift deleted file mode 100644 index 9bd19a0341..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Flows/Stripe/StripePurchaseFlow.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// StripePurchaseFlow.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import StoreKit -import Common - -public final class StripePurchaseFlow { - - public enum Error: Swift.Error { - case noProductsFound - case accountCreationFailed - } - - public static func subscriptionOptions() async -> Result { - os_log(.info, log: .subscription, "[StripePurchaseFlow] subscriptionOptions") - - guard case let .success(products) = await SubscriptionService.getProducts(), !products.isEmpty else { - os_log(.error, log: .subscription, "[StripePurchaseFlow] Error: noProductsFound") - return .failure(.noProductsFound) - } - - let currency = products.first?.currency ?? "USD" - - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.locale = Locale(identifier: "en_US@currency=\(currency)") - - let options: [SubscriptionOption] = products.map { - var displayPrice = "\($0.price) \($0.currency)" - - if let price = Float($0.price), let formattedPrice = formatter.string(from: price as NSNumber) { - displayPrice = formattedPrice - } - - let cost = SubscriptionOptionCost(displayPrice: displayPrice, recurrence: $0.billingPeriod.lowercased()) - - return SubscriptionOption(id: $0.productId, - cost: cost) - } - - let features = SubscriptionFeatureName.allCases.map { SubscriptionFeature(name: $0.rawValue) } - - return .success(SubscriptionOptions(platform: SubscriptionPlatformName.stripe.rawValue, - options: options, - features: features)) - } - - public static func prepareSubscriptionPurchase(emailAccessToken: String?) async -> Result { - os_log(.info, log: .subscription, "[StripePurchaseFlow] prepareSubscriptionPurchase") - - var authToken: String = "" - - switch await AuthService.createAccount(emailAccessToken: emailAccessToken) { - case .success(let response): - authToken = response.authToken - AccountManager().storeAuthToken(token: authToken) - case .failure: - os_log(.error, log: .subscription, "[StripePurchaseFlow] Error: accountCreationFailed") - return .failure(.accountCreationFailed) - } - - return .success(PurchaseUpdate(type: "redirect", token: authToken)) - } - - public static func completeSubscriptionPurchase() async { - os_log(.info, log: .subscription, "[StripePurchaseFlow] completeSubscriptionPurchase") - - let accountManager = AccountManager() - - if let authToken = accountManager.authToken { - if case let .success(accessToken) = await accountManager.exchangeAuthTokenToAccessToken(authToken), - case let .success(accountDetails) = await accountManager.fetchAccountDetails(with: accessToken) { - accountManager.storeAuthToken(token: authToken) - accountManager.storeAccount(token: accessToken, email: accountDetails.email, externalID: accountDetails.externalID) - } - } - - await AccountManager.checkForEntitlements(wait: 2.0, retry: 5) - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift deleted file mode 100644 index 20fc2f7b17..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/NSNotificationName+Subscription.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// NSNotificationName+Subscription.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -public extension NSNotification.Name { - - static let openPrivateBrowsing = Notification.Name("com.duckduckgo.subscription.open.private-browsing") - static let openPrivateSearch = Notification.Name("com.duckduckgo.subscription.open.private-search") - static let openEmailProtection = Notification.Name("com.duckduckgo.subscription.open.email-protection") - static let openAppTrackingProtection = Notification.Name("com.duckduckgo.subscription.open.app-tracking-protection") - static let openVPN = Notification.Name("com.duckduckgo.subscription.open.vpn") - static let openPersonalInformationRemoval = Notification.Name("com.duckduckgo.subscription.open.personal-information-removal") - static let openIdentityTheftRestoration = Notification.Name("com.duckduckgo.subscription.open.identity-theft-restoration") -} diff --git a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift b/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift deleted file mode 100644 index 5b04b7f5cf..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/PurchaseManager.swift +++ /dev/null @@ -1,260 +0,0 @@ -// -// PurchaseManager.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import StoreKit -import Common - -@available(macOS 12.0, iOS 15.0, *) typealias Transaction = StoreKit.Transaction -@available(macOS 12.0, iOS 15.0, *) typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo -@available(macOS 12.0, iOS 15.0, *) typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState - -public enum StoreError: Error { - case failedVerification -} - -enum PurchaseManagerError: Error { - case productNotFound - case externalIDisNotAValidUUID - case purchaseFailed - case transactionCannotBeVerified - case transactionPendingAuthentication - case purchaseCancelledByUser - case unknownError -} - -@available(macOS 12.0, iOS 15.0, *) -public final class PurchaseManager: ObservableObject { - - static let productIdentifiers = ["ios.subscription.1month", "ios.subscription.1year", - "subscription.1week", "subscription.1month", "subscription.1year", - "review.subscription.1week", "review.subscription.1month", "review.subscription.1year"] - - public static let shared = PurchaseManager() - - @Published public private(set) var availableProducts: [Product] = [] - @Published public private(set) var purchasedProductIDs: [String] = [] - @Published public private(set) var purchaseQueue: [String] = [] - - @Published private(set) var subscriptionGroupStatus: RenewalState? - - private var transactionUpdates: Task? - private var storefrontChanges: Task? - - public init() { - transactionUpdates = observeTransactionUpdates() - storefrontChanges = observeStorefrontChanges() - } - - deinit { - transactionUpdates?.cancel() - storefrontChanges?.cancel() - } - - @MainActor - @discardableResult - public func syncAppleIDAccount() async -> Result { - do { - purchaseQueue.removeAll() - - os_log(.info, log: .subscription, "[PurchaseManager] Before AppStore.sync()") - - try await AppStore.sync() - - os_log(.info, log: .subscription, "[PurchaseManager] After AppStore.sync()") - - await updatePurchasedProducts() - await updateAvailableProducts() - - return .success(()) - } catch { - os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) - return .failure(error) - } - } - - @MainActor - public func updateAvailableProducts() async { - os_log(.info, log: .subscription, "[PurchaseManager] updateAvailableProducts") - - do { - let availableProducts = try await Product.products(for: Self.productIdentifiers) - os_log(.info, log: .subscription, "[PurchaseManager] updateAvailableProducts fetched %d products", availableProducts.count) - - if self.availableProducts != availableProducts { - self.availableProducts = availableProducts - } - } catch { - os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) - } - } - - @MainActor - public func updatePurchasedProducts() async { - os_log(.info, log: .subscription, "[PurchaseManager] updatePurchasedProducts") - - var purchasedSubscriptions: [String] = [] - - do { - for await result in Transaction.currentEntitlements { - let transaction = try checkVerified(result) - - guard transaction.productType == .autoRenewable else { continue } - guard transaction.revocationDate == nil else { continue } - - if let expirationDate = transaction.expirationDate, expirationDate > .now { - purchasedSubscriptions.append(transaction.productID) - } - } - } catch { - os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) - } - - os_log(.info, log: .subscription, "[PurchaseManager] updatePurchasedProducts fetched %d active subscriptions", purchasedSubscriptions.count) - - if self.purchasedProductIDs != purchasedSubscriptions { - self.purchasedProductIDs = purchasedSubscriptions - } - - subscriptionGroupStatus = try? await availableProducts.first?.subscription?.status.first?.state - } - - @MainActor - public static func mostRecentTransaction() async -> String? { - os_log(.info, log: .subscription, "[PurchaseManager] mostRecentTransaction") - - var transactions: [VerificationResult] = [] - - for await result in Transaction.all { - transactions.append(result) - } - - os_log(.info, log: .subscription, "[PurchaseManager] mostRecentTransaction fetched %d transactions", transactions.count) - - return transactions.first?.jwsRepresentation - } - - @MainActor - public static func hasActiveSubscription() async -> Bool { - os_log(.info, log: .subscription, "[PurchaseManager] hasActiveSubscription") - - var transactions: [VerificationResult] = [] - - for await result in Transaction.currentEntitlements { - transactions.append(result) - } - - os_log(.info, log: .subscription, "[PurchaseManager] hasActiveSubscription fetched %d transactions", transactions.count) - - return !transactions.isEmpty - } - - @MainActor - public func purchaseSubscription(with identifier: String, externalID: String) async -> Result { - - guard let product = availableProducts.first(where: { $0.id == identifier }) else { return .failure(PurchaseManagerError.productNotFound) } - - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription %{public}s (%{public}s)", product.displayName, externalID) - - purchaseQueue.append(product.id) - - var options: Set = Set() - - if let token = UUID(uuidString: externalID) { - options.insert(.appAccountToken(token)) - } else { - os_log(.error, log: .subscription, "[PurchaseManager] Error: Failed to create UUID") - return .failure(PurchaseManagerError.externalIDisNotAValidUUID) - } - - let result: Product.PurchaseResult - do { - result = try await product.purchase(options: options) - } catch { - os_log(.error, log: .subscription, "[PurchaseManager] Error: %{public}s", String(reflecting: error)) - return .failure(PurchaseManagerError.purchaseFailed) - } - - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription complete") - - purchaseQueue.removeAll() - - switch result { - case let .success(.verified(transaction)): - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: success") - // Successful purchase - await transaction.finish() - await self.updatePurchasedProducts() - return .success(()) - case let .success(.unverified(_, error)): - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: success /unverified/ - %{public}s", String(reflecting: error)) - // Successful purchase but transaction/receipt can't be verified - // Could be a jailbroken phone - return .failure(PurchaseManagerError.transactionCannotBeVerified) - case .pending: - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: pending") - // Transaction waiting on SCA (Strong Customer Authentication) or - // approval from Ask to Buy - return .failure(PurchaseManagerError.transactionPendingAuthentication) - case .userCancelled: - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: user cancelled") - return .failure(PurchaseManagerError.purchaseCancelledByUser) - @unknown default: - os_log(.info, log: .subscription, "[PurchaseManager] purchaseSubscription result: unknown") - return .failure(PurchaseManagerError.unknownError) - } - } - - private func checkVerified(_ result: VerificationResult) throws -> T { - // Check whether the JWS passes StoreKit verification. - switch result { - case .unverified: - // StoreKit parses the JWS, but it fails verification. - throw StoreError.failedVerification - case .verified(let safe): - // The result is verified. Return the unwrapped value. - return safe - } - } - - private func observeTransactionUpdates() -> Task { - - Task.detached { [unowned self] in - for await result in Transaction.updates { - os_log(.info, log: .subscription, "[PurchaseManager] observeTransactionUpdates") - - if case .verified(let transaction) = result { - await transaction.finish() - } - - await self.updatePurchasedProducts() - } - } - } - - private func observeStorefrontChanges() -> Task { - - Task.detached { [unowned self] in - for await result in Storefront.updates { - os_log(.info, log: .subscription, "[PurchaseManager] observeStorefrontChanges: %s", result.countryCode) - await updatePurchasedProducts() - await updateAvailableProducts() - } - } - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift deleted file mode 100644 index 4d79e574b1..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Services/APIService.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// APIService.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -public enum APIServiceError: Swift.Error { - case decodingError - case encodingError - case serverError(description: String) - case unknownServerError - case connectionError -} - -struct ErrorResponse: Decodable { - let error: String -} - -public protocol APIService { - static var baseURL: URL { get } - static var session: URLSession { get } - static func executeAPICall(method: String, endpoint: String, headers: [String: String]?, body: Data?) async -> Result where T: Decodable -} - -public extension APIService { - - static func executeAPICall(method: String, endpoint: String, headers: [String: String]? = nil, body: Data? = nil) async -> Result where T: Decodable { - let request = makeAPIRequest(method: method, endpoint: endpoint, headers: headers, body: body) - - do { - let (data, urlResponse) = try await session.data(for: request) - - printDebugInfo(method: method, endpoint: endpoint, data: data, response: urlResponse) - - if let httpResponse = urlResponse as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) { - if let decodedResponse = decode(T.self, from: data) { - return .success(decodedResponse) - } else { - os_log(.error, log: .subscription, "Service error: APIServiceError.decodingError") - return .failure(.decodingError) - } - } else { - if let decodedResponse = decode(ErrorResponse.self, from: data) { - let errorDescription = "[\(endpoint)] \(urlResponse.httpStatusCodeAsString ?? ""): \(decodedResponse.error)" - os_log(.error, log: .subscription, "Service error: %{public}@", errorDescription) - return .failure(.serverError(description: errorDescription)) - } else { - os_log(.error, log: .subscription, "Service error: APIServiceError.unknownServerError") - return .failure(.unknownServerError) - } - } - } catch { - os_log(.error, log: .subscription, "Service error: %{public}@", error.localizedDescription) - return .failure(.connectionError) - } - } - - private static func makeAPIRequest(method: String, endpoint: String, headers: [String: String]?, body: Data?) -> URLRequest { - let url = baseURL.appendingPathComponent(endpoint) - var request = URLRequest(url: url) - request.httpMethod = method - if let headers = headers { - request.allHTTPHeaderFields = headers - } - if let body = body { - request.httpBody = body - } - - return request - } - - private static func decode(_: T.Type, from data: Data) -> T? where T: Decodable { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .millisecondsSince1970 - - return try? decoder.decode(T.self, from: data) - } - - private static func printDebugInfo(method: String, endpoint: String, data: Data, response: URLResponse) { - let statusCode = (response as? HTTPURLResponse)!.statusCode - let stringData = String(data: data, encoding: .utf8) ?? "" - - os_log(.info, log: .subscription, "[API] %d %{public}s /%{public}s :: %{public}s", statusCode, method, endpoint, stringData) - } - - static func makeAuthorizationHeader(for token: String) -> [String: String] { - ["Authorization": "Bearer " + token] - } -} - -extension URLResponse { - - var httpStatusCodeAsString: String? { - guard let httpStatusCode = (self as? HTTPURLResponse)?.statusCode else { return nil } - return String(httpStatusCode) - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift deleted file mode 100644 index 4e4755ad62..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Services/AuthService.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// AuthService.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -public struct AuthService: APIService { - - public static let session = { - let configuration = URLSessionConfiguration.ephemeral - return URLSession(configuration: configuration) - }() - public static let baseURL = URL(string: "https://quackdev.duckduckgo.com/api/auth")! - - // MARK: - - - public static func getAccessToken(token: String) async -> Result { - await executeAPICall(method: "GET", endpoint: "access-token", headers: makeAuthorizationHeader(for: token)) - } - - public struct AccessTokenResponse: Decodable { - public let accessToken: String - } - - // MARK: - - - public static func validateToken(accessToken: String) async -> Result { - await executeAPICall(method: "GET", endpoint: "validate-token", headers: makeAuthorizationHeader(for: accessToken)) - } - - // swiftlint:disable nesting - public struct ValidateTokenResponse: Decodable { - public let account: Account - - public struct Account: Decodable { - public let email: String? - let entitlements: [Entitlement] - public let externalID: String - - enum CodingKeys: String, CodingKey { - case email, entitlements, externalID = "externalId" // no underscores due to keyDecodingStrategy = .convertFromSnakeCase - } - } - - struct Entitlement: Decodable { - let id: Int - let name: String - let product: String - } - } - // swiftlint:enable nesting - - // MARK: - - - public static func createAccount(emailAccessToken: String?) async -> Result { - var headers: [String: String]? - - if let emailAccessToken { - headers = makeAuthorizationHeader(for: emailAccessToken) - } - - return await executeAPICall(method: "POST", endpoint: "account/create", headers: headers) - } - - public struct CreateAccountResponse: Decodable { - public let authToken: String - public let externalID: String - public let status: String - - enum CodingKeys: String, CodingKey { - case authToken = "authToken", externalID = "externalId", status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase - } - } - - // MARK: - - - public static func storeLogin(signature: String) async -> Result { - let bodyDict = ["signature": signature, - "store": "apple_app_store"] - - guard let bodyData = try? JSONEncoder().encode(bodyDict) else { return .failure(.encodingError) } - return await executeAPICall(method: "POST", endpoint: "store-login", body: bodyData) - } - - public struct StoreLoginResponse: Decodable { - public let authToken: String - public let email: String - public let externalID: String - public let id: Int - public let status: String - - enum CodingKeys: String, CodingKey { - case authToken = "authToken", email, externalID = "externalId", id, status // no underscores due to keyDecodingStrategy = .convertFromSnakeCase - } - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift b/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift deleted file mode 100644 index 62c7a7ca24..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/Services/SubscriptionService.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// SubscriptionService.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -public struct SubscriptionService: APIService { - - public static let session = { - let configuration = URLSessionConfiguration.ephemeral - return URLSession(configuration: configuration) - }() - public static let baseURL = URL(string: "https://subscriptions-dev.duckduckgo.com/api")! - - // MARK: - - - public static func getSubscriptionDetails(token: String) async -> Result { - let result: Result = await executeAPICall(method: "GET", endpoint: "subscription", headers: makeAuthorizationHeader(for: token)) - - switch result { - case .success(let response): - cachedSubscriptionDetailsResponse = response - case .failure: - cachedSubscriptionDetailsResponse = nil - } - - return result - } - - public struct GetSubscriptionDetailsResponse: Decodable { - public let productId: String - public let startedAt: Date - public let expiresOrRenewsAt: Date - public let platform: Platform - public let status: String - - public var isSubscriptionActive: Bool { - status.lowercased() != "expired" && status.lowercased() != "inactive" - } - - public enum Platform: String, Codable { - case apple, google, stripe - case unknown - - public init(from decoder: Decoder) throws { - self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown - } - } - } - - public static var cachedSubscriptionDetailsResponse: GetSubscriptionDetailsResponse? - - // MARK: - - - public static func getProducts() async -> Result { - await executeAPICall(method: "GET", endpoint: "products") - } - - public typealias GetProductsResponse = [GetProductsItem] - - public struct GetProductsItem: Decodable { - public let productId: String - public let productLabel: String - public let billingPeriod: String - public let price: String - public let currency: String - } - - // MARK: - - - public static func getCustomerPortalURL(accessToken: String, externalID: String) async -> Result { - var headers = makeAuthorizationHeader(for: accessToken) - headers["externalAccountId"] = externalID - return await executeAPICall(method: "GET", endpoint: "checkout/portal", headers: headers) - } - - public struct GetCustomerPortalURLResponse: Decodable { - public let customerPortalUrl: String - } - -} diff --git a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift b/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift deleted file mode 100644 index 4414fd9650..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/SubscriptionPurchaseEnvironment.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// SubscriptionPurchaseEnvironment.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -public final class SubscriptionPurchaseEnvironment { - - public enum Environment: String { - case appStore, stripe - } - - public static var current: Environment = .appStore { - didSet { - os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] Setting to %{public}s", current.rawValue) - - canPurchase = false - - switch current { - case .appStore: - setupForAppStore() - case .stripe: - setupForStripe() - } - } - } - - public static var canPurchase: Bool = false { - didSet { - os_log(.info, log: .subscription, "[SubscriptionPurchaseEnvironment] canPurchase %{public}s", (canPurchase ? "true" : "false")) - } - } - - private static func setupForAppStore() { - if #available(macOS 12.0, iOS 15.0, *) { - Task { - await PurchaseManager.shared.updateAvailableProducts() - canPurchase = !PurchaseManager.shared.availableProducts.isEmpty - } - } - } - - private static func setupForStripe() { - Task { - if case let .success(products) = await SubscriptionService.getProducts() { - canPurchase = !products.isEmpty - } - } - } -} diff --git a/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift b/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift deleted file mode 100644 index 0a84fe3746..0000000000 --- a/LocalPackages/Subscription/Sources/Subscription/URL+Subscription.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// URL+Subscription.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -public extension URL { - - static var subscriptionBaseURL: URL { - URL(string: "https://abrown.duckduckgo.com/subscriptions")! - } - - static var subscriptionPurchase: URL { - subscriptionBaseURL.appendingPathComponent("welcome") - } - - static var subscriptionFAQ: URL { - URL(string: "https://duckduckgo.com/about")! - } - - // MARK: - Subscription Email - static var activateSubscriptionViaEmail: URL { - subscriptionBaseURL.appendingPathComponent("activate") - } - - static var addEmailToSubscription: URL { - subscriptionBaseURL.appendingPathComponent("add-email") - } - - static var manageSubscriptionEmail: URL { - subscriptionBaseURL.appendingPathComponent("manage") - } - - // MARK: - App Store app manage subscription URL - - static var manageSubscriptionsInAppStoreAppURL: URL { - URL(string: "macappstores://apps.apple.com/account/subscriptions")! - } - - // MARK: - Identity Theft Restoration - - static var identityTheftRestoration: URL { - URL(string: "https://abrown.duckduckgo.com/identity-theft-restoration")! - } -} diff --git a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift b/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift deleted file mode 100644 index e209d149d3..0000000000 --- a/LocalPackages/Subscription/Tests/SubscriptionTests/SubscriptionTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SubscriptionTests.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -@testable import Subscription - -final class SubscriptionTests: XCTestCase { -} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 04bd6b6e02..663ce90fef 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,14 +12,14 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(path: "../Subscription"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ .target( name: "SubscriptionUI", dependencies: [ - .product(name: "Subscription", package: "Subscription"), + .product(name: "Subscription", package: "BrowserServicesKit"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") ], resources: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index d2ce313228..5eb9e15177 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index b07a7136a2..0a162fce7d 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index e395277e9c..dff7a8f9a1 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index 26559c296b..8d16fa88c3 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "106.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "107.0.0"), ], targets: [ .target(