From 88cb05c0994aa5b933edd662f68ad679915d09a7 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Mon, 3 Jun 2024 13:03:09 -0600 Subject: [PATCH 01/12] Added uploader class to intents extension --- ...NSURLSessionConfiguration+Extensions.swift | 21 ++++++ IntentsExtension/Models/Note+Intents.swift | 49 +++++++++++++ IntentsExtension/Tools/IntentsConstants.swift | 2 + IntentsExtension/Tools/Uploader.swift | 71 +++++++++++++++++++ Simplenote.xcodeproj/project.pbxproj | 10 +++ 5 files changed, 153 insertions(+) create mode 100644 IntentsExtension/Extensions/NSURLSessionConfiguration+Extensions.swift create mode 100644 IntentsExtension/Tools/Uploader.swift diff --git a/IntentsExtension/Extensions/NSURLSessionConfiguration+Extensions.swift b/IntentsExtension/Extensions/NSURLSessionConfiguration+Extensions.swift new file mode 100644 index 000000000..971289349 --- /dev/null +++ b/IntentsExtension/Extensions/NSURLSessionConfiguration+Extensions.swift @@ -0,0 +1,21 @@ +// +// NSURLSessionConfiguration+Extensions.swift +// IntentsExtension +// +// Created by Charlie Scheer on 6/3/24. +// Copyright © 2024 Simperium. All rights reserved. +// + +import Foundation + +extension URLSessionConfiguration { + /// Returns a new Background Session Configuration, with a random identifier. + /// + class func backgroundSessionConfigurationWithRandomizedIdentifier() -> URLSessionConfiguration { + let identifier = IntentsConstants.extensionGroupName + "." + UUID().uuidString + let configuration = URLSessionConfiguration.background(withIdentifier: identifier) + configuration.sharedContainerIdentifier = IntentsConstants.extensionGroupName + + return configuration + } +} diff --git a/IntentsExtension/Models/Note+Intents.swift b/IntentsExtension/Models/Note+Intents.swift index 0ac2c7397..32fb03539 100644 --- a/IntentsExtension/Models/Note+Intents.swift +++ b/IntentsExtension/Models/Note+Intents.swift @@ -53,6 +53,55 @@ extension Note { let result = String(content[range]) return result.droppingPrefix(Constants.titleMarkdownPrefix) } + + private func objectFromJSONString(_ json: String) -> Any? { + guard let data = json.data(using: .utf8) else { + return nil + } + + return try? JSONSerialization.jsonObject(with: data) + } + + var tagsArray: [String] { + guard let tagsString = tags, + let array = objectFromJSONString(tagsString) as? [String] else { + return [] + } + + return array + } + + var systemTagsArray: [String] { + guard let systemTagsString = systemTags, + let array = objectFromJSONString(systemTagsString) as? [String] else { + return [] + } + + return array + } + + func toDictionary() -> [String: Any] { + + return [ + "tags": tagsArray, + "deleted": 0, + "shareURL": shareURL ?? String(), + "publishURL": publishURL ?? String(), + "content": content ?? "", + "systemTags": systemTagsArray, + "creationDate": (creationDate ?? .now).timeIntervalSince1970, + "modificationDate": (modificationDate ?? .now).timeIntervalSince1970 + ] + } + + func toJsonData() -> Data? { + do { + return try JSONSerialization.data(withJSONObject: toDictionary(), options: .prettyPrinted) + } catch { + print("Error converting Note to JSON: \(error)") + return nil + } + } } private struct Constants { diff --git a/IntentsExtension/Tools/IntentsConstants.swift b/IntentsExtension/Tools/IntentsConstants.swift index 664275a30..726e04be8 100644 --- a/IntentsExtension/Tools/IntentsConstants.swift +++ b/IntentsExtension/Tools/IntentsConstants.swift @@ -10,4 +10,6 @@ import Foundation struct IntentsConstants { static let noteIdentifierKey = "OpenNoteIntentHandlerIdentifierKey" + static let extensionGroupName = Bundle.main.sharedGroupDomain + static let simperiumAppID = "chalk-bump-f49" } diff --git a/IntentsExtension/Tools/Uploader.swift b/IntentsExtension/Tools/Uploader.swift new file mode 100644 index 000000000..b3a6844c1 --- /dev/null +++ b/IntentsExtension/Tools/Uploader.swift @@ -0,0 +1,71 @@ +import Foundation + +/// The purpose of this class is to encapsulate NSURLSession's interaction code, required to upload +/// a note to Simperium's REST endpoint. +/// +class Uploader: NSObject { + + /// Simperium's Token + /// + private let token: String + + /// Designated Initializer + /// + init(simperiumToken: String) { + token = simperiumToken + } + + // MARK: - Public Methods + func send(_ note: Note) { + // Build the targetURL + let endpoint = String(format: "%@/%@/%@/i/%@", Settings.simperiumBaseURL, IntentsConstants.simperiumAppID, Settings.bucketName, note.simperiumKey) + let targetURL = URL(string: endpoint.lowercased())! + + // Request + var request = URLRequest(url: targetURL) + request.httpMethod = Settings.httpMethodPost + request.httpBody = note.toJsonData() + request.setValue(token, forHTTPHeaderField: Settings.authHeader) + + // Task! + let sc = URLSessionConfiguration.backgroundSessionConfigurationWithRandomizedIdentifier() + + let session = Foundation.URLSession(configuration: sc, delegate: self, delegateQueue: .main) + let task = session.downloadTask(with: request) + task.resume() + } +} + +// MARK: - URLSessionDelegate +// +extension Uploader: URLSessionDelegate { + + @objc + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + print("<> Uploader.didBecomeInvalidWithError: \(String(describing: error))") + } + + @objc + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + print("<> Uploader.URLSessionDidFinishEventsForBackgroundURLSession") + } +} + +// MARK: - URLSessionTaskDelegate +// +extension Uploader: URLSessionTaskDelegate { + + @objc + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + print("<> Uploader.didCompleteWithError: \(String(describing: error))") + } +} + +// MARK: - Settings +// +private struct Settings { + static let authHeader = "X-Simperium-Token" + static let bucketName = "note" + static let httpMethodPost = "POST" + static let simperiumBaseURL = "https://api.simperium.com/1/" +} diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 93828e572..4bfb400eb 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -268,6 +268,8 @@ BA71EC252BC88FFC00F42CB1 /* NSManagedObjectContext+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */; }; BA78AF712B5B2BC300DCF896 /* AutomatticTracks in Frameworks */ = {isa = PBXBuildFile; productRef = BA78AF702B5B2BC300DCF896 /* AutomatticTracks */; }; BA938CEE26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */; }; + BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB642C0E46CC00B34EA7 /* Uploader.swift */; }; + BA94CB682C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */; }; BAA0A88B26BA39150006260E /* Date+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */; }; BAA0A88F26BA39200006260E /* AccountDeletionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88126B9F8970006260E /* AccountDeletionController.swift */; }; BAA0A89326BA39260006260E /* RemoteError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A87926B9F0B50006260E /* RemoteError.swift */; }; @@ -710,6 +712,9 @@ BA8CF21B2BFD20770087F33D /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; BA938CEB26ACFF4A00BE5A1D /* Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Remote.swift; sourceTree = ""; }; BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountVerificationController+TestHelpers.swift"; sourceTree = ""; }; + BA94CB642C0E46CC00B34EA7 /* Uploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uploader.swift; sourceTree = ""; }; + BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSURLSessionConfiguration+Extensions.swift"; sourceTree = ""; }; + BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; BAA0A87926B9F0B50006260E /* RemoteError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteError.swift; sourceTree = ""; }; BAA0A88126B9F8970006260E /* AccountDeletionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDeletionController.swift; sourceTree = ""; }; BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = ""; }; @@ -822,6 +827,7 @@ 26F72A8B14032D2A00A7935E /* Frameworks */ = { isa = PBXGroup; children = ( + BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */, 26F72A8F14032D2A00A7935E /* AppKit.framework */, 26F72A8C14032D2A00A7935E /* Cocoa.framework */, 26F72A9014032D2A00A7935E /* CoreData.framework */, @@ -1594,6 +1600,7 @@ BAAA71E52C07A35F00244C01 /* ExtensionCoreDataWrapper.swift */, BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */, BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */, + BA94CB642C0E46CC00B34EA7 /* Uploader.swift */, ); path = Tools; sourceTree = ""; @@ -1603,6 +1610,7 @@ children = ( BAAA71EC2C07A69C00244C01 /* IntentNote+Helpers.swift */, BAAD56482C0A42E40047E024 /* FileManager+Intents.swift */, + BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -2207,6 +2215,7 @@ BAAA71F22C07A7FD00244C01 /* IntentsConstants.swift in Sources */, BAAA71D92C07A14600244C01 /* String+Simplenote.swift in Sources */, BACA687C2C0A764C005409C1 /* KeychainPasswordItem.swift in Sources */, + BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */, BAAA71E92C07A39300244C01 /* FileManager+Simplenote.swift in Sources */, BAAA71E82C07A37700244C01 /* StorageSettings.swift in Sources */, BAAA71E22C07A2BA00244C01 /* NSSortDescriptor+Simplenote.swift in Sources */, @@ -2231,6 +2240,7 @@ BAAA71E72C07A37000244C01 /* CoreDataManager.swift in Sources */, BAAA71D52C07A10E00244C01 /* SPManagedObject+Intents.swift in Sources */, BAAA71ED2C07A69C00244C01 /* IntentNote+Helpers.swift in Sources */, + BA94CB682C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From d4e0a657b77d49caad5291daba1d8367c43e3356 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Mon, 3 Jun 2024 13:04:41 -0600 Subject: [PATCH 02/12] Added downloader to the project --- IntentsExtension/Tools/Downloader.swift | 44 +++++++++++++++++++ IntentsExtension/Tools/IntentsConstants.swift | 1 + IntentsExtension/Tools/Uploader.swift | 3 +- Simplenote.xcodeproj/project.pbxproj | 4 ++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 IntentsExtension/Tools/Downloader.swift diff --git a/IntentsExtension/Tools/Downloader.swift b/IntentsExtension/Tools/Downloader.swift new file mode 100644 index 000000000..ce9804b09 --- /dev/null +++ b/IntentsExtension/Tools/Downloader.swift @@ -0,0 +1,44 @@ +import Foundation + +class Downloader: NSObject { + + /// Simperium's Token + /// + private let token: String + + /// Designated Initializer + /// + init(simperiumToken: String) { + token = simperiumToken + } + + func getNoteContent(for simperiumKey: String) async throws -> String? { + let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, IntentsConstants.simperiumAppID, Settings.bucketName, simperiumKey) + let targetURL = URL(string: endpoint.lowercased())! + + // Request + var request = URLRequest(url: targetURL) + request.httpMethod = Settings.httpMethodGet + request.setValue(token, forHTTPHeaderField: Settings.authHeader) + + let sc = URLSessionConfiguration.default + let session = Foundation.URLSession(configuration: sc, delegate: nil, delegateQueue: .main) + + let downloadedData = try await session.data(for: request) + + return try extractNoteContent(from: downloadedData.0) + } + + func extractNoteContent(from data: Data) throws -> String? { + let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] + return jsonObject?["content"] as? String + } +} + +// MARK: - Settings +// +private struct Settings { + static let authHeader = "X-Simperium-Token" + static let bucketName = "note" + static let httpMethodGet = "GET" +} diff --git a/IntentsExtension/Tools/IntentsConstants.swift b/IntentsExtension/Tools/IntentsConstants.swift index 726e04be8..f350b811b 100644 --- a/IntentsExtension/Tools/IntentsConstants.swift +++ b/IntentsExtension/Tools/IntentsConstants.swift @@ -12,4 +12,5 @@ struct IntentsConstants { static let noteIdentifierKey = "OpenNoteIntentHandlerIdentifierKey" static let extensionGroupName = Bundle.main.sharedGroupDomain static let simperiumAppID = "chalk-bump-f49" + static let simperiumBaseURL = "https://api.simperium.com/1/" } diff --git a/IntentsExtension/Tools/Uploader.swift b/IntentsExtension/Tools/Uploader.swift index b3a6844c1..69e790c39 100644 --- a/IntentsExtension/Tools/Uploader.swift +++ b/IntentsExtension/Tools/Uploader.swift @@ -18,7 +18,7 @@ class Uploader: NSObject { // MARK: - Public Methods func send(_ note: Note) { // Build the targetURL - let endpoint = String(format: "%@/%@/%@/i/%@", Settings.simperiumBaseURL, IntentsConstants.simperiumAppID, Settings.bucketName, note.simperiumKey) + let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, IntentsConstants.simperiumAppID, Settings.bucketName, note.simperiumKey) let targetURL = URL(string: endpoint.lowercased())! // Request @@ -67,5 +67,4 @@ private struct Settings { static let authHeader = "X-Simperium-Token" static let bucketName = "note" static let httpMethodPost = "POST" - static let simperiumBaseURL = "https://api.simperium.com/1/" } diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 4bfb400eb..5fa019d58 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -270,6 +270,7 @@ BA938CEE26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA938CED26AD055400BE5A1D /* AccountVerificationController+TestHelpers.swift */; }; BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB642C0E46CC00B34EA7 /* Uploader.swift */; }; BA94CB682C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */; }; + BA94CB6F2C0E4AFA00B34EA7 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */; }; BAA0A88B26BA39150006260E /* Date+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */; }; BAA0A88F26BA39200006260E /* AccountDeletionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A88126B9F8970006260E /* AccountDeletionController.swift */; }; BAA0A89326BA39260006260E /* RemoteError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA0A87926B9F0B50006260E /* RemoteError.swift */; }; @@ -715,6 +716,7 @@ BA94CB642C0E46CC00B34EA7 /* Uploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uploader.swift; sourceTree = ""; }; BA94CB672C0E47DD00B34EA7 /* NSURLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSURLSessionConfiguration+Extensions.swift"; sourceTree = ""; }; BA94CB6B2C0E4A6100B34EA7 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; BAA0A87926B9F0B50006260E /* RemoteError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteError.swift; sourceTree = ""; }; BAA0A88126B9F8970006260E /* AccountDeletionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountDeletionController.swift; sourceTree = ""; }; BAA0A88626B9F8B40006260E /* Date+Simplenote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = ""; }; @@ -1601,6 +1603,7 @@ BAAA71EE2C07A6CF00244C01 /* IntentsError.swift */, BAAA71F12C07A7FD00244C01 /* IntentsConstants.swift */, BA94CB642C0E46CC00B34EA7 /* Uploader.swift */, + BA94CB6E2C0E4AFA00B34EA7 /* Downloader.swift */, ); path = Tools; sourceTree = ""; @@ -2218,6 +2221,7 @@ BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */, BAAA71E92C07A39300244C01 /* FileManager+Simplenote.swift in Sources */, BAAA71E82C07A37700244C01 /* StorageSettings.swift in Sources */, + BA94CB6F2C0E4AFA00B34EA7 /* Downloader.swift in Sources */, BAAA71E22C07A2BA00244C01 /* NSSortDescriptor+Simplenote.swift in Sources */, BAAA71E62C07A35F00244C01 /* ExtensionCoreDataWrapper.swift in Sources */, BAB261722BFFD0F4009A98D7 /* ShortcutIntents.intentdefinition in Sources */, From 891389cc14e298e023e46b1231acf1b7ee5fe7f7 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Mon, 3 Jun 2024 15:09:02 -0600 Subject: [PATCH 03/12] Added append note intent definition --- IntentsExtension/IntentHandler.swift | 2 + .../AppendNoteIntentHandler.swift | 40 +++++ IntentsExtension/Support Files/Info.plist | 1 + IntentsExtension/Tools/IntentsConstants.swift | 2 +- Simplenote.xcodeproj/project.pbxproj | 4 + Simplenote/ShortcutIntents.intentdefinition | 162 ++++++++++++++++++ Simplenote/Simplenote-Info.plist | 1 + 7 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift diff --git a/IntentsExtension/IntentHandler.swift b/IntentsExtension/IntentHandler.swift index 0888415cb..ec29eaca3 100644 --- a/IntentsExtension/IntentHandler.swift +++ b/IntentsExtension/IntentHandler.swift @@ -17,6 +17,8 @@ class IntentHandler: INExtension { return OpenNoteIntentHandler() case is FindNoteIntent: return FindNoteIntentHandler() + case is AppendNoteIntent: + return AppendNoteIntentHandler() default: return self } diff --git a/IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift b/IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift new file mode 100644 index 000000000..8fba54e70 --- /dev/null +++ b/IntentsExtension/IntentHandlers/AppendNoteIntentHandler.swift @@ -0,0 +1,40 @@ +import Intents + +class AppendNoteIntentHandler: NSObject, AppendNoteIntentHandling { + let coreDataWrapper = ExtensionCoreDataWrapper() + + func resolveContent(for intent: AppendNoteIntent) async -> INStringResolutionResult { + guard let content = intent.content else { + return INStringResolutionResult.needsValue() + } + return INStringResolutionResult.success(with: content) + } + + func resolveNote(for intent: AppendNoteIntent) async -> IntentNoteResolutionResult { + IntentNoteResolutionResult.resolve(intent.note, in: coreDataWrapper) + } + + func provideNoteOptionsCollection(for intent: AppendNoteIntent) async throws -> INObjectCollection { + let intentNotes = try IntentNote.allNotes(in: coreDataWrapper) + return INObjectCollection(items: intentNotes) + } + + func handle(intent: AppendNoteIntent) async -> AppendNoteIntentResponse { + guard let identifier = intent.note?.identifier, + let content = intent.content, + let note = coreDataWrapper.resultsController?.note(forSimperiumKey: identifier), + let token = KeychainManager.extensionToken else { + return AppendNoteIntentResponse(code: .failure, userActivity: nil) + } + + guard let existingContent = try? await Downloader(simperiumToken: token).getNoteContent(for: identifier) else { + return AppendNoteIntentResponse(code: .failure, userActivity: nil) + } + + note.content = existingContent + "\n\(content)" + let uploader = Uploader(simperiumToken: token) + uploader.send(note) + + return AppendNoteIntentResponse(code: .success, userActivity: nil) + } +} diff --git a/IntentsExtension/Support Files/Info.plist b/IntentsExtension/Support Files/Info.plist index bf36e2edf..288422e45 100644 --- a/IntentsExtension/Support Files/Info.plist +++ b/IntentsExtension/Support Files/Info.plist @@ -12,6 +12,7 @@ IntentsSupported + AppendNoteIntent FindNoteIntent OpenNewNoteIntent OpenNoteIntent diff --git a/IntentsExtension/Tools/IntentsConstants.swift b/IntentsExtension/Tools/IntentsConstants.swift index f350b811b..81a3343a5 100644 --- a/IntentsExtension/Tools/IntentsConstants.swift +++ b/IntentsExtension/Tools/IntentsConstants.swift @@ -12,5 +12,5 @@ struct IntentsConstants { static let noteIdentifierKey = "OpenNoteIntentHandlerIdentifierKey" static let extensionGroupName = Bundle.main.sharedGroupDomain static let simperiumAppID = "chalk-bump-f49" - static let simperiumBaseURL = "https://api.simperium.com/1/" + static let simperiumBaseURL = "https://api.simperium.com/1" } diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 5fa019d58..c27b2d697 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -256,6 +256,7 @@ BA2C65CF26FE996A00FA84E1 /* NSButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2C65CA26FE996100FA84E1 /* NSButton+Extensions.swift */; }; BA4C6D16264CA8C000B723A7 /* SignupRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D15264CA8C000B723A7 /* SignupRemoteTests.swift */; }; BA4C6D18264CAAF800B723A7 /* URLRequest+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */; }; + BA54F2462C0E63C700DBCE9D /* AppendNoteIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */; }; BA553F0927065E20007737E9 /* FontSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA553F0727065E20007737E9 /* FontSettings.swift */; }; BA5A658C2C090F4800F605A6 /* SharedStorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */; }; BA5A658F2C090FEC00F605A6 /* FileManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A658E2C090FEC00F605A6 /* FileManagerProtocol.swift */; }; @@ -702,6 +703,7 @@ BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Simplenote.swift"; sourceTree = ""; }; BA52005A2BC878F1003F1B75 /* CSSearchable+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CSSearchable+Helpers.swift"; sourceTree = ""; }; BA52005C2BC88397003F1B75 /* NSManagedObjectContext+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Simplenote.swift"; sourceTree = ""; }; + BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppendNoteIntentHandler.swift; sourceTree = ""; }; BA553F0727065E20007737E9 /* FontSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSettings.swift; sourceTree = ""; }; BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStorageMigratorTests.swift; sourceTree = ""; }; BA5A658E2C090FEC00F605A6 /* FileManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerProtocol.swift; sourceTree = ""; }; @@ -1637,6 +1639,7 @@ BAB261742BFFD319009A98D7 /* OpenNewNoteIntentHandler.swift */, BAC5DFB82C079E5A002AD7EF /* OpenNoteIntentHandler.swift */, BA2BF33F2C07C75500A7C894 /* FindNoteIntentHandler.swift */, + BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */, ); path = IntentHandlers; sourceTree = ""; @@ -2236,6 +2239,7 @@ BAAA71D72C07A11500244C01 /* Note+Intents.swift in Sources */, BAAA71D82C07A12F00244C01 /* NoteContentHelper.swift in Sources */, BAAA71EA2C07A58100244C01 /* Simplenote.xcdatamodeld in Sources */, + BA54F2462C0E63C700DBCE9D /* AppendNoteIntentHandler.swift in Sources */, BAB261662BFFD0AF009A98D7 /* IntentHandler.swift in Sources */, BAAD56472C0A42000047E024 /* FileManagerProtocol.swift in Sources */, BAC5DFB52C069568002AD7EF /* Bundle+Simplenote.swift in Sources */, diff --git a/Simplenote/ShortcutIntents.intentdefinition b/Simplenote/ShortcutIntents.intentdefinition index 7a74d72e9..92a864354 100644 --- a/Simplenote/ShortcutIntents.intentdefinition +++ b/Simplenote/ShortcutIntents.intentdefinition @@ -325,6 +325,168 @@ INIntentVerb Do + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Append content to an existing note in Simplenote + INIntentDescriptionID + QW5IlD + INIntentIneligibleForSuggestions + + INIntentInput + note + INIntentLastParameterTag + 3 + INIntentManagedParameterCombinations + + note,content + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Append ${content} to ${note} + INIntentParameterCombinationTitleID + yhhw7r + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + AppendNote + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Note + INIntentParameterDisplayNameID + 67s2SR + INIntentParameterDisplayPriority + 1 + INIntentParameterName + note + INIntentParameterObjectType + IntentNote + INIntentParameterObjectTypeNamespace + ihfyvu + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${note}’. + INIntentParameterPromptDialogFormatStringID + cWzR46 + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${note}’? + INIntentParameterPromptDialogFormatStringID + lDws53 + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterTag + 2 + INIntentParameterType + Object + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Content + INIntentParameterDisplayNameID + whQq9I + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValueID + 3ABNcl + INIntentParameterMetadataMultiline + + + INIntentParameterName + content + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + What would you like to append to ${note} + INIntentParameterPromptDialogFormatStringID + N2YveT + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Append To Note + INIntentTitleID + rjuYKo + INIntentType + Custom + INIntentVerb + Do + INTypes diff --git a/Simplenote/Simplenote-Info.plist b/Simplenote/Simplenote-Info.plist index 66c273091..c126c791d 100644 --- a/Simplenote/Simplenote-Info.plist +++ b/Simplenote/Simplenote-Info.plist @@ -56,6 +56,7 @@ SPApplication NSUserActivityTypes + AppendNoteIntent FindNoteIntent OpenNewNoteIntent OpenNoteIntent From ced38bb26e011428e16aaf0a6599bda880d4d5ce Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Mon, 3 Jun 2024 15:09:14 -0600 Subject: [PATCH 04/12] Updated entitlements to allow intents extension network access --- .../Support Files/IntentsExtension.entitlements | 6 ++++++ .../Support Files/IntentsExtensionDebug.entitlements | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/IntentsExtension/Support Files/IntentsExtension.entitlements b/IntentsExtension/Support Files/IntentsExtension.entitlements index cb817ac5e..5de6d1d45 100644 --- a/IntentsExtension/Support Files/IntentsExtension.entitlements +++ b/IntentsExtension/Support Files/IntentsExtension.entitlements @@ -8,6 +8,12 @@ $(TeamIdentifierPrefix)com.automattic.SimplenoteMac + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + keychain-access-groups $(AppIdentifierPrefix)com.automattic.SimplenoteMac diff --git a/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements b/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements index 0b8d9cd0d..c6038abb6 100644 --- a/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements +++ b/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements @@ -8,6 +8,12 @@ $(TeamIdentifierPrefix)com.automattic.SimplenoteMac.Development + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + keychain-access-groups $(AppIdentifierPrefix)com.automattic.SimplenoteMac.Development From badb0055d4cd633ccbd1481b689276b104a885e1 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 6 Jun 2024 14:31:56 -0600 Subject: [PATCH 05/12] Readded intents extension scheme... not sure how that got removed... --- .../xcschemes/IntentsExtension.xcscheme | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 Simplenote.xcodeproj/xcshareddata/xcschemes/IntentsExtension.xcscheme diff --git a/Simplenote.xcodeproj/xcshareddata/xcschemes/IntentsExtension.xcscheme b/Simplenote.xcodeproj/xcshareddata/xcschemes/IntentsExtension.xcscheme new file mode 100644 index 000000000..084e1eab5 --- /dev/null +++ b/Simplenote.xcodeproj/xcshareddata/xcschemes/IntentsExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e5ddac0bb251fa480577e7029e69a8c3d54dbfcd Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 6 Jun 2024 14:32:05 -0600 Subject: [PATCH 06/12] Updated package.resolved --- Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved index d2db68724..76e690e81 100644 --- a/Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Simplenote.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "7fc7ca43967e2980d8691a8e017c118a84133aac", - "version" : "8.26.0" + "revision" : "a62862c99f5bcb28fd78617fab1a5fe29607c06c", + "version" : "8.28.0" } }, { From eacb50008a3d13a639574e19455f2d68c56cd5d6 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 6 Jun 2024 14:34:32 -0600 Subject: [PATCH 07/12] Moved creating json object from string to string extension --- IntentsExtension/Models/Note+Intents.swift | 4 ++-- Simplenote/String+Simplenote.swift | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/IntentsExtension/Models/Note+Intents.swift b/IntentsExtension/Models/Note+Intents.swift index 32fb03539..0214c51b9 100644 --- a/IntentsExtension/Models/Note+Intents.swift +++ b/IntentsExtension/Models/Note+Intents.swift @@ -64,7 +64,7 @@ extension Note { var tagsArray: [String] { guard let tagsString = tags, - let array = objectFromJSONString(tagsString) as? [String] else { + let array = tagsString.objectFromJSONString() as? [String] else { return [] } @@ -73,7 +73,7 @@ extension Note { var systemTagsArray: [String] { guard let systemTagsString = systemTags, - let array = objectFromJSONString(systemTagsString) as? [String] else { + let array = systemTagsString.objectFromJSONString as? [String] else { return [] } diff --git a/Simplenote/String+Simplenote.swift b/Simplenote/String+Simplenote.swift index 17662425c..27da8cf34 100644 --- a/Simplenote/String+Simplenote.swift +++ b/Simplenote/String+Simplenote.swift @@ -90,6 +90,14 @@ extension String { func replacingNewlinesWithSpaces() -> String { split(whereSeparator: { $0.isNewline }).joined(separator: " ") } + + func objectFromJSONString() -> Any? { + guard let data = self.data(using: .utf8) else { + return nil + } + + return try? JSONSerialization.jsonObject(with: data) + } } // MARK: - Searching for the first / last characters From 2d8be0960efa0f850fee1dd69fff12320f860667 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 6 Jun 2024 14:35:52 -0600 Subject: [PATCH 08/12] Removed server entitlement from intents extension --- IntentsExtension/Support Files/IntentsExtension.entitlements | 2 -- .../Support Files/IntentsExtensionDebug.entitlements | 2 -- 2 files changed, 4 deletions(-) diff --git a/IntentsExtension/Support Files/IntentsExtension.entitlements b/IntentsExtension/Support Files/IntentsExtension.entitlements index 5de6d1d45..29675a5ce 100644 --- a/IntentsExtension/Support Files/IntentsExtension.entitlements +++ b/IntentsExtension/Support Files/IntentsExtension.entitlements @@ -12,8 +12,6 @@ com.apple.security.network.client - com.apple.security.network.server - keychain-access-groups $(AppIdentifierPrefix)com.automattic.SimplenoteMac diff --git a/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements b/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements index c6038abb6..8ad56bac5 100644 --- a/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements +++ b/IntentsExtension/Support Files/IntentsExtensionDebug.entitlements @@ -12,8 +12,6 @@ com.apple.security.network.client - com.apple.security.network.server - keychain-access-groups $(AppIdentifierPrefix)com.automattic.SimplenoteMac.Development From 0ff7dcba0809bbb7436d736df782e002dfe9416e Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 6 Jun 2024 14:37:22 -0600 Subject: [PATCH 09/12] Simplified a bit of code in the downloader.swift file --- IntentsExtension/Tools/Downloader.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IntentsExtension/Tools/Downloader.swift b/IntentsExtension/Tools/Downloader.swift index ce9804b09..45097f68d 100644 --- a/IntentsExtension/Tools/Downloader.swift +++ b/IntentsExtension/Tools/Downloader.swift @@ -21,15 +21,14 @@ class Downloader: NSObject { request.httpMethod = Settings.httpMethodGet request.setValue(token, forHTTPHeaderField: Settings.authHeader) - let sc = URLSessionConfiguration.default - let session = Foundation.URLSession(configuration: sc, delegate: nil, delegateQueue: .main) + let session = Foundation.URLSession(configuration: .default, delegate: nil, delegateQueue: .main) let downloadedData = try await session.data(for: request) return try extractNoteContent(from: downloadedData.0) } - func extractNoteContent(from data: Data) throws -> String? { + private func extractNoteContent(from data: Data) throws -> String? { let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] return jsonObject?["content"] as? String } From 24db053a4bdda7703b3ad95d77ef4a4444b33b03 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 6 Jun 2024 14:38:32 -0600 Subject: [PATCH 10/12] Removed simplenote app id from intents constants --- IntentsExtension/Tools/Downloader.swift | 2 +- IntentsExtension/Tools/IntentsConstants.swift | 1 - IntentsExtension/Tools/Uploader.swift | 2 +- Simplenote.xcodeproj/project.pbxproj | 2 ++ 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/IntentsExtension/Tools/Downloader.swift b/IntentsExtension/Tools/Downloader.swift index 45097f68d..6f80656ad 100644 --- a/IntentsExtension/Tools/Downloader.swift +++ b/IntentsExtension/Tools/Downloader.swift @@ -13,7 +13,7 @@ class Downloader: NSObject { } func getNoteContent(for simperiumKey: String) async throws -> String? { - let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, IntentsConstants.simperiumAppID, Settings.bucketName, simperiumKey) + let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, SPCredentials.simperiumAppID, Settings.bucketName, simperiumKey) let targetURL = URL(string: endpoint.lowercased())! // Request diff --git a/IntentsExtension/Tools/IntentsConstants.swift b/IntentsExtension/Tools/IntentsConstants.swift index 81a3343a5..28aff5fd6 100644 --- a/IntentsExtension/Tools/IntentsConstants.swift +++ b/IntentsExtension/Tools/IntentsConstants.swift @@ -11,6 +11,5 @@ import Foundation struct IntentsConstants { static let noteIdentifierKey = "OpenNoteIntentHandlerIdentifierKey" static let extensionGroupName = Bundle.main.sharedGroupDomain - static let simperiumAppID = "chalk-bump-f49" static let simperiumBaseURL = "https://api.simperium.com/1" } diff --git a/IntentsExtension/Tools/Uploader.swift b/IntentsExtension/Tools/Uploader.swift index 69e790c39..2526cc013 100644 --- a/IntentsExtension/Tools/Uploader.swift +++ b/IntentsExtension/Tools/Uploader.swift @@ -18,7 +18,7 @@ class Uploader: NSObject { // MARK: - Public Methods func send(_ note: Note) { // Build the targetURL - let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, IntentsConstants.simperiumAppID, Settings.bucketName, note.simperiumKey) + let endpoint = String(format: "%@/%@/%@/i/%@", IntentsConstants.simperiumBaseURL, SPCredentials.simperiumAppID, Settings.bucketName, note.simperiumKey) let targetURL = URL(string: endpoint.lowercased())! // Request diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index c27b2d697..1a1652d67 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -256,6 +256,7 @@ BA2C65CF26FE996A00FA84E1 /* NSButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2C65CA26FE996100FA84E1 /* NSButton+Extensions.swift */; }; BA4C6D16264CA8C000B723A7 /* SignupRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D15264CA8C000B723A7 /* SignupRemoteTests.swift */; }; BA4C6D18264CAAF800B723A7 /* URLRequest+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4C6D17264CAAF800B723A7 /* URLRequest+Simplenote.swift */; }; + BA4F223E2C1255A500144EDA /* SPCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E196C0230F5F5300F5658A /* SPCredentials.swift */; }; BA54F2462C0E63C700DBCE9D /* AppendNoteIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA54F2452C0E63C700DBCE9D /* AppendNoteIntentHandler.swift */; }; BA553F0927065E20007737E9 /* FontSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA553F0727065E20007737E9 /* FontSettings.swift */; }; BA5A658C2C090F4800F605A6 /* SharedStorageMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5A658B2C090F4800F605A6 /* SharedStorageMigratorTests.swift */; }; @@ -2221,6 +2222,7 @@ BAAA71F22C07A7FD00244C01 /* IntentsConstants.swift in Sources */, BAAA71D92C07A14600244C01 /* String+Simplenote.swift in Sources */, BACA687C2C0A764C005409C1 /* KeychainPasswordItem.swift in Sources */, + BA4F223E2C1255A500144EDA /* SPCredentials.swift in Sources */, BA94CB652C0E46CC00B34EA7 /* Uploader.swift in Sources */, BAAA71E92C07A39300244C01 /* FileManager+Simplenote.swift in Sources */, BAAA71E82C07A37700244C01 /* StorageSettings.swift in Sources */, From 1c020835e7b47dcb50d7687a9c0329b5d5923f2c Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 6 Jun 2024 14:42:11 -0600 Subject: [PATCH 11/12] Fixed issue calling string method to create json object --- IntentsExtension/Models/Note+Intents.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IntentsExtension/Models/Note+Intents.swift b/IntentsExtension/Models/Note+Intents.swift index 0214c51b9..f839876b9 100644 --- a/IntentsExtension/Models/Note+Intents.swift +++ b/IntentsExtension/Models/Note+Intents.swift @@ -73,7 +73,7 @@ extension Note { var systemTagsArray: [String] { guard let systemTagsString = systemTags, - let array = systemTagsString.objectFromJSONString as? [String] else { + let array = systemTagsString.objectFromJSONString() as? [String] else { return [] } From 17ece7a4f84086cf691df4cc862b2e4d0cb62030 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:23:38 -0600 Subject: [PATCH 12/12] Fix demo credentials --- Simplenote/SPCredentials-demo.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Simplenote/SPCredentials-demo.swift b/Simplenote/SPCredentials-demo.swift index 46608d15a..a27b459d8 100644 --- a/Simplenote/SPCredentials-demo.swift +++ b/Simplenote/SPCredentials-demo.swift @@ -1,5 +1,8 @@ /// Simplenote API Demo Credentials /// + +import Foundation + @objcMembers class SPCredentials: NSObject {