From 5c62780217b19cbdbd98ac5791f1f11aad21ae16 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 10 Nov 2021 18:45:50 +0300 Subject: [PATCH] issue #910 Crash when opening Contacts screen (#971) Co-authored-by: Tom --- FlowCrypt.xcodeproj/project.pbxproj | 16 +++--- FlowCrypt/Extensions/RealmExtension.swift | 51 +++++++++++++++++++ .../KeyService.swift | 2 +- .../LocalContactsProvider.swift | 7 ++- appium/tests/data/index.ts | 4 ++ appium/tests/screenobjects/contacts.screen.ts | 12 ++--- .../SelectRecipientByName.spec.ts | 32 +++++++++--- 7 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 FlowCrypt/Extensions/RealmExtension.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 226c31fc6..c5009719e 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 2C124DB42728809100A2EFA6 /* ApiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C124DB32728809100A2EFA6 /* ApiCall.swift */; }; 2C2A3B4B2719EE6100B7F27B /* KeyServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */; }; 2C2A3B4D2719EF7300B7F27B /* PassPhraseServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */; }; + 2C595764273AC47400C7C055 /* RealmExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C595763273AC47400C7C055 /* RealmExtension.swift */; }; 2C60AB0C272564D40040D7F2 /* InvalidStorageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */; }; 2CC12C3F273571B80021DDDF /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC12C3E273571B80021DDDF /* AttachmentManager.swift */; }; 32DCA00224982EDA88D69C6E /* AppErr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA4B11D4531B3B04D01D1 /* AppErr.swift */; }; @@ -446,6 +447,7 @@ 2C124DB32728809100A2EFA6 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = ""; }; 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyServiceTests.swift; sourceTree = ""; }; 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseServiceMock.swift; sourceTree = ""; }; + 2C595763273AC47400C7C055 /* RealmExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmExtension.swift; sourceTree = ""; }; 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidStorageViewController.swift; sourceTree = ""; }; 2CC12C3E273571B80021DDDF /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; 32DCA058652FD4616FB04FB6 /* SequenceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SequenceExtensions.swift; sourceTree = ""; }; @@ -991,15 +993,16 @@ 32DCAEF9FEEF84D4F0D4A516 /* Extensions */ = { isa = PBXGroup; children = ( - 32DCA7E0AFE19FACB0F233ED /* URLSessionExtension.swift */, - 9FEED1B7230C08D700700F8E /* UIViewControllerExtensions.swift */, - 518389C72726D7DD00131B2C /* UIViewController+Spinner.swift */, - 9F31AB9D232BF2A600CF87EA /* UIColorExtension.swift */, D2D27B78248A8694007346FA /* BigIntExtension.swift */, 9F2F217226B3269D0044E144 /* CombineExtensions.swift */, - 518389C92726D8F700131B2C /* UIApplicationExtension.swift */, - 51B0C7702729861C00124663 /* String+Extension.swift */, 51E4F0B427348E310017DABB /* Error+Extension.swift */, + 2C595763273AC47400C7C055 /* RealmExtension.swift */, + 51B0C7702729861C00124663 /* String+Extension.swift */, + 518389C92726D8F700131B2C /* UIApplicationExtension.swift */, + 9F31AB9D232BF2A600CF87EA /* UIColorExtension.swift */, + 518389C72726D7DD00131B2C /* UIViewController+Spinner.swift */, + 9FEED1B7230C08D700700F8E /* UIViewControllerExtensions.swift */, + 32DCA7E0AFE19FACB0F233ED /* URLSessionExtension.swift */, ); path = Extensions; sourceTree = ""; @@ -2568,6 +2571,7 @@ 5ADEDCBC23A4329000EC495E /* PublicKeyDetailViewController.swift in Sources */, 21489B80267CC39E00BDE4AC /* ClientConfigurationService.swift in Sources */, D28655932423B4EE0066F52E /* MyMenuViewDecorator.swift in Sources */, + 2C595764273AC47400C7C055 /* RealmExtension.swift in Sources */, 04B4728D1ECE29D200B8266F /* KeyInfo.swift in Sources */, 9F3EF32F23B172D300FA0CEF /* SearchViewController.swift in Sources */, F191F621272511790053833E /* BlurViewController.swift in Sources */, diff --git a/FlowCrypt/Extensions/RealmExtension.swift b/FlowCrypt/Extensions/RealmExtension.swift new file mode 100644 index 000000000..0c6f91bdc --- /dev/null +++ b/FlowCrypt/Extensions/RealmExtension.swift @@ -0,0 +1,51 @@ +// +// RealmExtension.swift +// FlowCrypt +// +// Created by  Ivan Ushakov on 09.11.2021 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Realm +import RealmSwift + +protocol RealmListDetachable { + func detached() -> Self +} + +extension List: RealmListDetachable where Element: Object { + func detached() -> List { + let detached = self.detached + let result = List() + result.append(objectsIn: detached) + return result + } +} + +extension Object { + // TODO Temporary solution from StackOverflow for https://github.com/FlowCrypt/flowcrypt-ios/issues/877 + func detached() -> Self { + let detached = type(of: self).init() + for property in objectSchema.properties { + guard + property != objectSchema.primaryKeyProperty, + let value = value(forKey: property.name) + else { continue } + + if let detachable = value as? Object { + detached.setValue(detachable.detached(), forKey: property.name) + } else if let list = value as? RealmListDetachable { + detached.setValue(list.detached(), forKey: property.name) + } else { + detached.setValue(value, forKey: property.name) + } + } + return detached + } +} + +extension Sequence where Iterator.Element: Object { + var detached: [Element] { + return self.map({ $0.detached() }) + } +} diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift index ee4777351..78e8da8ca 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift @@ -82,7 +82,7 @@ final class KeyService: KeyServiceType { // get keys associated with this account, freeze them to pass across threads let keysInfo = storage.keysInfo().filter { $0.account == email }.map { object -> KeyInfo in guard object.realm != nil else { return object } - return object.freeze() + return object.detached() } guard let foundKey = try await findKeyByUserEmail(keysInfo: keysInfo, email: email) else { return nil diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index b5a2e3377..246ed9df0 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -75,8 +75,7 @@ extension LocalContactsProvider: LocalContactsProviderType { func searchRecipient(with email: String) async throws -> RecipientWithSortedPubKeys? { guard let recipientObject = find(with: email) else { return nil } - // TODO temporary fix for Realm thread problem - return try await parseRecipient(from: recipientObject.freeze()) + return try await parseRecipient(from: recipientObject.detached()) } func searchEmails(query: String) -> [String] { @@ -87,10 +86,10 @@ extension LocalContactsProvider: LocalContactsProviderType { } func getAllRecipients() async throws -> [RecipientWithSortedPubKeys] { - let objects = Array(localContactsCache.realm.objects(RecipientObject.self)) + let objects = localContactsCache.realm.objects(RecipientObject.self).detached var recipients: [RecipientWithSortedPubKeys] = [] for object in objects { - recipients.append(try await parseRecipient(from: object.freeze())) + recipients.append(try await parseRecipient(from: object)) } return recipients.sorted(by: { $0.email > $1.email }) } diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 38f59c9d7..4a5f44991 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -11,6 +11,10 @@ export const CommonData = { email: 'dmitry@flowcrypt.com', name: 'Dima' }, + secondContact: { + email: 'demo@flowcrypt.com', + name: 'Demo' + }, bundleId: { id: 'com.flowcrypt.as.ios.debug', }, diff --git a/appium/tests/screenobjects/contacts.screen.ts b/appium/tests/screenobjects/contacts.screen.ts index b6c000f7c..5d0242811 100644 --- a/appium/tests/screenobjects/contacts.screen.ts +++ b/appium/tests/screenobjects/contacts.screen.ts @@ -24,8 +24,8 @@ class ContactsScreen extends BaseScreen { return $(SELECTORS.EMPTY_CONTACTS_LIST); } - contactEmail(email) { - return $(`~${email}`) + contactName(name) { + return $(`~${name}`) } checkContactScreen() { @@ -41,12 +41,12 @@ class ContactsScreen extends BaseScreen { ElementHelper.waitAndClick(this.backButton); } - checkContact(email) { - this.contactEmail(email).waitForDisplayed(); + checkContact(name) { + this.contactName(name).waitForDisplayed(); } - clickOnContact(email) { - ElementHelper.waitAndClick(this.contactEmail(email)); + clickOnContact(name) { + ElementHelper.waitAndClick(this.contactName(name)); } } diff --git a/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts b/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts index 0b4f3ee80..86b686ed3 100644 --- a/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts +++ b/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts @@ -15,12 +15,18 @@ describe('COMPOSE EMAIL: ', () => { it('user is able to select recipient from contact list using contact name', () => { - const contactEmail = CommonData.contact.email; - const contactName = CommonData.contact.name; + const firstContactEmail = CommonData.contact.email; + const firstContactName = CommonData.contact.name; + const firstContactItemName = 'Dmitry at FlowCrypt'; + + const secondContactEmail = CommonData.secondContact.email; + const secondContactName = CommonData.secondContact.name; + const secondContactItemName = 'Demo key 2'; SplashScreen.login(); CreateKeyScreen.setPassPhrase(); + // Go to Contacts screen MenuBarScreen.clickMenuIcon(); MenuBarScreen.checkUserEmail(); @@ -35,12 +41,21 @@ describe('COMPOSE EMAIL: ', () => { MenuBarScreen.clickMenuIcon(); MenuBarScreen.clickInboxButton(); + // Add first contact InboxScreen.clickCreateEmail(); - NewMessageScreen.setAddRecipientByName(contactName, contactEmail); - NewMessageScreen.checkAddedRecipient(contactEmail); + NewMessageScreen.setAddRecipientByName(firstContactName, firstContactEmail); + NewMessageScreen.checkAddedRecipient(firstContactEmail); NewMessageScreen.clickBackButton(); + // Add second contact + InboxScreen.clickCreateEmail(); + + NewMessageScreen.setAddRecipientByName(secondContactName, secondContactEmail); + NewMessageScreen.checkAddedRecipient(secondContactEmail); + NewMessageScreen.clickBackButton(); + + // Go to Contacts screen MenuBarScreen.clickMenuIcon(); MenuBarScreen.checkUserEmail(); @@ -49,10 +64,13 @@ describe('COMPOSE EMAIL: ', () => { SettingsScreen.clickOnSettingItem('Contacts'); ContactScreen.checkContactScreen(); - ContactScreen.checkContact(contactEmail); - ContactScreen.clickOnContact(contactEmail); + ContactScreen.checkContact(firstContactItemName); + ContactScreen.checkContact(secondContactItemName); + + // Go to Contact screen + ContactScreen.clickOnContact(firstContactItemName); - ContactPublicKeyScreen.checkPgpUserId(contactEmail); + ContactPublicKeyScreen.checkPgpUserId(firstContactEmail); ContactPublicKeyScreen.checkPublicKeyDetailsNotEmpty(); ContactPublicKeyScreen.clickOnFingerPrint(); ContactPublicKeyScreen.checkPublicKeyNotEmpty();